feature: add alphabetical sort for user and team collections (#5383)
Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: Nivedin <53208152+nivedin@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
637c380c07
commit
81fe98f25d
63 changed files with 3478 additions and 341 deletions
|
|
@ -35,6 +35,7 @@ import { UserLastActiveOnInterceptor } from './interceptors/user-last-active-on.
|
|||
import { InfraTokenModule } from './infra-token/infra-token.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { PubSubModule } from './pubsub/pubsub.module';
|
||||
import { SortModule } from './orchestration/sort/sort.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -122,6 +123,7 @@ import { PubSubModule } from './pubsub/pubsub.module';
|
|||
HealthModule,
|
||||
AccessTokenModule,
|
||||
InfraTokenModule,
|
||||
SortModule,
|
||||
],
|
||||
providers: [
|
||||
GQLComplexityPlugin,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import { UserSettingsUserResolver } from './user-settings/user.resolver';
|
|||
import { InfraResolver } from './admin/infra.resolver';
|
||||
import { InfraConfigResolver } from './infra-config/infra-config.resolver';
|
||||
import { InfraTokenResolver } from './infra-token/infra-token.resolver';
|
||||
import { SortTeamCollectionResolver } from './orchestration/sort/sort-team-collection.resolver';
|
||||
import { SortUserCollectionResolver } from './orchestration/sort/sort-user-collection.resolver';
|
||||
|
||||
/**
|
||||
* All the resolvers present in the application.
|
||||
|
|
@ -62,6 +64,8 @@ const RESOLVERS = [
|
|||
UserSettingsUserResolver,
|
||||
InfraConfigResolver,
|
||||
InfraTokenResolver,
|
||||
SortUserCollectionResolver,
|
||||
SortTeamCollectionResolver,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Mutation, Resolver, Subscription } from '@nestjs/graphql';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { TeamCollection } from 'src/team-collection/team-collection.model';
|
||||
import { SortService } from './sort.service';
|
||||
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
|
||||
import { TeamAccessRole } from 'src/team/team.model';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { GqlTeamMemberGuard } from 'src/team/guards/gql-team-member.guard';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => TeamCollection)
|
||||
export class SortTeamCollectionResolver {
|
||||
constructor(
|
||||
private readonly sortService: SortService,
|
||||
private readonly pubSub: PubSubService,
|
||||
) {}
|
||||
|
||||
// Mutations
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Sort team collections',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamAccessRole.OWNER, TeamAccessRole.EDITOR)
|
||||
async sortTeamCollections(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'parentCollectionID',
|
||||
description: 'ID of the parent collection',
|
||||
type: () => ID,
|
||||
nullable: true,
|
||||
})
|
||||
parentCollectionID: string | null,
|
||||
@Args({
|
||||
name: 'sortOption',
|
||||
description: 'Sorting option',
|
||||
type: () => SortOptions,
|
||||
})
|
||||
sortOption: SortOptions,
|
||||
): Promise<boolean> {
|
||||
const result = await this.sortService.sortTeamCollections(
|
||||
teamID,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
);
|
||||
|
||||
if (E.isLeft(result)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
@Subscription(() => Boolean, {
|
||||
description: 'Listen for Team Root Collection Sort Events',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@RequiresTeamRole(
|
||||
TeamAccessRole.OWNER,
|
||||
TeamAccessRole.EDITOR,
|
||||
TeamAccessRole.VIEWER,
|
||||
)
|
||||
@SkipThrottle()
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
teamRootCollectionsSorted(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team to listen to',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
return this.pubSub.asyncIterator(`team_coll_root/${teamID}/sorted`);
|
||||
}
|
||||
|
||||
@Subscription(() => ID, {
|
||||
description: 'Listen for Team Child Collection Sort Events',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@RequiresTeamRole(
|
||||
TeamAccessRole.OWNER,
|
||||
TeamAccessRole.EDITOR,
|
||||
TeamAccessRole.VIEWER,
|
||||
)
|
||||
@SkipThrottle()
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
teamChildCollectionsSorted(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team to listen to',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
return this.pubSub.asyncIterator(`team_coll_child/${teamID}/sorted`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Mutation, Resolver, Subscription } from '@nestjs/graphql';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SortService } from './sort.service';
|
||||
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { UserCollection } from 'src/user-collection/user-collections.model';
|
||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { UserCollectionSortData } from './sort.model';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => UserCollection)
|
||||
export class SortUserCollectionResolver {
|
||||
constructor(
|
||||
private readonly sortService: SortService,
|
||||
private readonly pubSub: PubSubService,
|
||||
) {}
|
||||
|
||||
// Mutations
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Sort user collections',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async sortUserCollections(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'parentCollectionID',
|
||||
description: 'ID of the parent collection',
|
||||
type: () => ID,
|
||||
nullable: true,
|
||||
})
|
||||
parentCollectionID: string | null,
|
||||
@Args({
|
||||
name: 'sortOption',
|
||||
description: 'Sorting option',
|
||||
type: () => SortOptions,
|
||||
})
|
||||
sortOption: SortOptions,
|
||||
): Promise<boolean> {
|
||||
const result = await this.sortService.sortUserCollections(
|
||||
user.uid,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
);
|
||||
|
||||
if (E.isLeft(result)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
@Subscription(() => UserCollectionSortData, {
|
||||
description: 'Listen for User Root Collection Sort Events',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@SkipThrottle()
|
||||
@UseGuards(GqlAuthGuard)
|
||||
userRootCollectionsSorted(@GqlUser() user: AuthUser) {
|
||||
return this.pubSub.asyncIterator(`user_coll_root/${user.uid}/sorted`);
|
||||
}
|
||||
|
||||
@Subscription(() => UserCollectionSortData, {
|
||||
description: 'Listen for User Child Collection Sort Events',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@SkipThrottle()
|
||||
@UseGuards(GqlAuthGuard)
|
||||
userChildCollectionsSorted(@GqlUser() user: AuthUser) {
|
||||
return this.pubSub.asyncIterator(`user_coll_child/${user.uid}/sorted`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
@ObjectType()
|
||||
export class UserCollectionSortData {
|
||||
@Field(() => ID, {
|
||||
description: 'ID of the parent collection',
|
||||
nullable: true,
|
||||
})
|
||||
parentCollectionID: string;
|
||||
|
||||
@Field(() => SortOptions, {
|
||||
description: 'Sorting option',
|
||||
})
|
||||
sortOption: SortOptions;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SortTeamCollectionResolver } from './sort-team-collection.resolver';
|
||||
import { SortService } from './sort.service';
|
||||
import { TeamCollectionModule } from 'src/team-collection/team-collection.module';
|
||||
import { TeamRequestModule } from 'src/team-request/team-request.module';
|
||||
import { SortUserCollectionResolver } from './sort-user-collection.resolver';
|
||||
import { UserCollectionModule } from 'src/user-collection/user-collection.module';
|
||||
import { UserRequestModule } from 'src/user-request/user-request.module';
|
||||
import { TeamModule } from 'src/team/team.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserCollectionModule,
|
||||
UserRequestModule,
|
||||
TeamModule,
|
||||
TeamCollectionModule,
|
||||
TeamRequestModule,
|
||||
],
|
||||
providers: [
|
||||
SortUserCollectionResolver,
|
||||
SortTeamCollectionResolver,
|
||||
SortService,
|
||||
],
|
||||
})
|
||||
export class SortModule {}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { TeamCollectionService } from 'src/team-collection/team-collection.service';
|
||||
import { TeamRequestService } from 'src/team-request/team-request.service';
|
||||
import { UserRequestService } from 'src/user-request/user-request.service';
|
||||
import { UserCollectionService } from 'src/user-collection/user-collection.service';
|
||||
import { SortService } from './sort.service';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import {
|
||||
TEAM_COL_REORDERING_FAILED,
|
||||
TEAM_REQ_REORDERING_FAILED,
|
||||
} from 'src/errors';
|
||||
|
||||
const mockUserRequestService = mockDeep<UserRequestService>();
|
||||
const mockUserCollectionService = mockDeep<UserCollectionService>();
|
||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||
const mockTeamRequestService = mockDeep<TeamRequestService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
|
||||
const sortService = new SortService(
|
||||
mockUserCollectionService,
|
||||
mockUserRequestService,
|
||||
mockTeamCollectionService,
|
||||
mockTeamRequestService,
|
||||
mockPubSub,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockPubSub.publish.mockClear();
|
||||
});
|
||||
|
||||
describe('sortTeamCollections', () => {
|
||||
it('should return left if teamCollectionService.sortTeamCollections fails', async () => {
|
||||
mockTeamCollectionService.sortTeamCollections.mockResolvedValue(
|
||||
E.left(TEAM_COL_REORDERING_FAILED),
|
||||
);
|
||||
const result = await sortService.sortTeamCollections(
|
||||
'teamID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.left(TEAM_COL_REORDERING_FAILED));
|
||||
expect(mockTeamCollectionService.sortTeamCollections).toHaveBeenCalledWith(
|
||||
'teamID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
});
|
||||
it('should return left if teamRequestService.sortTeamRequests fails', async () => {
|
||||
mockTeamCollectionService.sortTeamCollections.mockResolvedValue(
|
||||
E.right(true),
|
||||
);
|
||||
mockTeamRequestService.sortTeamRequests.mockResolvedValue(
|
||||
E.left(TEAM_REQ_REORDERING_FAILED),
|
||||
);
|
||||
const result = await sortService.sortTeamCollections(
|
||||
'teamID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.left(TEAM_REQ_REORDERING_FAILED));
|
||||
expect(mockTeamRequestService.sortTeamRequests).toHaveBeenCalledWith(
|
||||
'teamID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
});
|
||||
it('should publish root event if parentCollectionID is falsy', async () => {
|
||||
mockTeamCollectionService.sortTeamCollections.mockResolvedValue(
|
||||
E.right(true),
|
||||
);
|
||||
mockTeamRequestService.sortTeamRequests.mockResolvedValue(E.right(true));
|
||||
const result = await sortService.sortTeamCollections(
|
||||
'teamID',
|
||||
null,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll_root/teamID/sorted`,
|
||||
true,
|
||||
);
|
||||
expect(result).toEqual(E.right(true));
|
||||
});
|
||||
it('should publish child event if parentCollectionID is truthy', async () => {
|
||||
mockTeamCollectionService.sortTeamCollections.mockResolvedValue(
|
||||
E.right(true),
|
||||
);
|
||||
mockTeamRequestService.sortTeamRequests.mockResolvedValue(E.right(true));
|
||||
const result = await sortService.sortTeamCollections(
|
||||
'teamID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll_child/teamID/sorted`,
|
||||
'parentCollectionID',
|
||||
);
|
||||
expect(result).toEqual(E.right(true));
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortUserCollections', () => {
|
||||
it('should return left if userCollectionService.sortUserCollections fails', async () => {
|
||||
mockUserCollectionService.sortUserCollections.mockResolvedValue(
|
||||
E.left('user_coll/reordering_failed'),
|
||||
);
|
||||
const result = await sortService.sortUserCollections(
|
||||
'userID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.left('user_coll/reordering_failed'));
|
||||
expect(mockUserCollectionService.sortUserCollections).toHaveBeenCalledWith(
|
||||
'userID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return left if userRequestService.sortUserRequests fails', async () => {
|
||||
mockUserCollectionService.sortUserCollections.mockResolvedValue(
|
||||
E.right(true),
|
||||
);
|
||||
mockUserRequestService.sortUserRequests.mockResolvedValue(
|
||||
E.left('user_coll/reordering_failed'),
|
||||
);
|
||||
const result = await sortService.sortUserCollections(
|
||||
'userID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.left('user_coll/reordering_failed'));
|
||||
expect(mockUserRequestService.sortUserRequests).toHaveBeenCalledWith(
|
||||
'userID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish root event if parentCollectionID is falsy', async () => {
|
||||
mockUserCollectionService.sortUserCollections.mockResolvedValue(
|
||||
E.right(true),
|
||||
);
|
||||
mockUserRequestService.sortUserRequests.mockResolvedValue(E.right(true));
|
||||
const result = await sortService.sortUserCollections(
|
||||
'userID',
|
||||
null,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`user_coll_root/userID/sorted`,
|
||||
{
|
||||
parentCollectionID: null,
|
||||
sortOption: SortOptions.TITLE_ASC,
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(E.right(true));
|
||||
});
|
||||
|
||||
it('should publish child event if parentCollectionID is truthy', async () => {
|
||||
mockUserCollectionService.sortUserCollections.mockResolvedValue(
|
||||
E.right(true),
|
||||
);
|
||||
mockUserRequestService.sortUserRequests.mockResolvedValue(E.right(true));
|
||||
const result = await sortService.sortUserCollections(
|
||||
'userID',
|
||||
'parentCollectionID',
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`user_coll_child/userID/sorted`,
|
||||
{
|
||||
parentCollectionID: 'parentCollectionID',
|
||||
sortOption: SortOptions.TITLE_ASC,
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(E.right(true));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TeamCollectionService } from 'src/team-collection/team-collection.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
import { TeamRequestService } from 'src/team-request/team-request.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { UserRequestService } from 'src/user-request/user-request.service';
|
||||
import { UserCollectionService } from 'src/user-collection/user-collection.service';
|
||||
|
||||
@Injectable()
|
||||
export class SortService {
|
||||
constructor(
|
||||
private readonly userCollectionService: UserCollectionService,
|
||||
private readonly userRequestService: UserRequestService,
|
||||
private readonly teamCollectionService: TeamCollectionService,
|
||||
private readonly teamRequestService: TeamRequestService,
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
async sortTeamCollections(
|
||||
teamID: string,
|
||||
parentCollectionID: string,
|
||||
sortOption: SortOptions,
|
||||
) {
|
||||
const isCollectionSorted =
|
||||
await this.teamCollectionService.sortTeamCollections(
|
||||
teamID,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
);
|
||||
|
||||
if (E.isLeft(isCollectionSorted)) return E.left(isCollectionSorted.left);
|
||||
|
||||
const isRequestSorted = await this.teamRequestService.sortTeamRequests(
|
||||
teamID,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
);
|
||||
|
||||
if (E.isLeft(isRequestSorted)) return E.left(isRequestSorted.left);
|
||||
|
||||
// Publish the sort event
|
||||
if (!parentCollectionID) {
|
||||
this.pubsub.publish(`team_coll_root/${teamID}/sorted`, true);
|
||||
} else {
|
||||
this.pubsub.publish(
|
||||
`team_coll_child/${teamID}/sorted`,
|
||||
parentCollectionID,
|
||||
);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
async sortUserCollections(
|
||||
userID: string,
|
||||
parentCollectionID: string,
|
||||
sortOption: SortOptions,
|
||||
) {
|
||||
const isCollectionSorted =
|
||||
await this.userCollectionService.sortUserCollections(
|
||||
userID,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
);
|
||||
|
||||
if (E.isLeft(isCollectionSorted)) return E.left(isCollectionSorted.left);
|
||||
|
||||
const isRequestSorted = await this.userRequestService.sortUserRequests(
|
||||
userID,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
);
|
||||
|
||||
if (E.isLeft(isRequestSorted)) return E.left(isRequestSorted.left);
|
||||
|
||||
// Publish the sort event
|
||||
if (!parentCollectionID) {
|
||||
this.pubsub.publish(`user_coll_root/${userID}/sorted`, {
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
});
|
||||
} else {
|
||||
this.pubsub.publish(`user_coll_child/${userID}/sorted`, {
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
});
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import {
|
|||
UserCollectionReorderData,
|
||||
} from 'src/user-collection/user-collections.model';
|
||||
import { Shortcode } from 'src/shortcode/shortcode.model';
|
||||
import { UserCollectionSortData } from 'src/orchestration/sort/sort.model';
|
||||
|
||||
// A custom message type that defines the topic and the corresponding payload.
|
||||
// For every module that publishes a subscription add its type def and the possible subscription type.
|
||||
|
|
@ -53,6 +54,8 @@ export type TopicDef = {
|
|||
[topic: `user_coll/${string}/${'duplicated'}`]: UserCollectionDuplicatedData;
|
||||
[topic: `user_coll/${string}/${'deleted'}`]: UserCollectionRemovedData;
|
||||
[topic: `user_coll/${string}/${'order_updated'}`]: UserCollectionReorderData;
|
||||
[topic: `user_coll_root/${string}/${'sorted'}`]: UserCollectionSortData;
|
||||
[topic: `user_coll_child/${string}/${'sorted'}`]: UserCollectionSortData;
|
||||
[topic: `team/${string}/member_removed`]: string;
|
||||
[topic: `team/${string}/${'member_added' | 'member_updated'}`]: TeamMember;
|
||||
[
|
||||
|
|
@ -64,6 +67,8 @@ export type TopicDef = {
|
|||
[topic: `team_coll/${string}/${'coll_removed'}`]: string;
|
||||
[topic: `team_coll/${string}/${'coll_moved'}`]: TeamCollection;
|
||||
[topic: `team_coll/${string}/${'coll_order_updated'}`]: CollectionReorderData;
|
||||
[topic: `team_coll_root/${string}/${'sorted'}`]: boolean;
|
||||
[topic: `team_coll_child/${string}/${'sorted'}`]: string;
|
||||
[
|
||||
topic: `team_req/${string}/${'req_created' | 'req_updated' | 'req_moved'}`
|
||||
]: TeamRequest;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { AuthUser } from 'src/types/AuthUser';
|
|||
import { TeamCollectionService } from './team-collection.service';
|
||||
import { TeamCollection } from './team-collection.model';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
|
|
@ -1518,6 +1519,75 @@ describe('updateTeamCollection', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('sortTeamCollections', () => {
|
||||
it('should sort collections by TITLE_ASC', async () => {
|
||||
const parentID = null;
|
||||
const teamID = team.id;
|
||||
|
||||
mockPrisma.$transaction.mockImplementation(async (cb) => cb(mockPrisma));
|
||||
mockPrisma.acquireLocks.mockResolvedValue(undefined);
|
||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce(
|
||||
rootTeamCollectionList,
|
||||
);
|
||||
|
||||
const result = await teamCollectionService.sortTeamCollections(
|
||||
teamID,
|
||||
parentID,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
|
||||
expect(result).toEqual(E.right(true));
|
||||
expect(mockPrisma.teamCollection.findMany).toHaveBeenCalledWith({
|
||||
where: { teamID, parentID },
|
||||
orderBy: { title: 'asc' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(mockPrisma.teamCollection.update).toHaveBeenCalledTimes(
|
||||
rootTeamCollectionList.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should sort collections by TITLE_DESC', async () => {
|
||||
const parentID = null;
|
||||
const teamID = team.id;
|
||||
|
||||
mockPrisma.$transaction.mockImplementation(async (cb) => cb(mockPrisma));
|
||||
mockPrisma.acquireLocks.mockResolvedValue(undefined);
|
||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce(
|
||||
rootTeamCollectionList,
|
||||
);
|
||||
|
||||
const result = await teamCollectionService.sortTeamCollections(
|
||||
teamID,
|
||||
parentID,
|
||||
SortOptions.TITLE_DESC,
|
||||
);
|
||||
|
||||
expect(result).toEqual(E.right(true));
|
||||
expect(mockPrisma.teamCollection.findMany).toHaveBeenCalledWith({
|
||||
where: { teamID, parentID },
|
||||
orderBy: { title: 'desc' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(mockPrisma.teamCollection.update).toHaveBeenCalledTimes(
|
||||
rootTeamCollectionList.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return left(TEAM_COL_REORDERING_FAILED) on error', async () => {
|
||||
const parentID = null;
|
||||
const teamID = team.id;
|
||||
|
||||
mockPrisma.$transaction.mockRejectedValueOnce(new Error('fail'));
|
||||
const result = await teamCollectionService.sortTeamCollections(
|
||||
teamID,
|
||||
parentID,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.left(TEAM_COL_REORDERING_FAILED));
|
||||
});
|
||||
});
|
||||
|
||||
//ToDo: write test cases for exportCollectionsToJSON
|
||||
|
||||
describe('getCollectionForCLI', () => {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import {
|
|||
import { RESTError } from 'src/types/RESTError';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import { PrismaError } from 'src/prisma/prisma-error-codes';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
@Injectable()
|
||||
export class TeamCollectionService {
|
||||
|
|
@ -1467,4 +1468,57 @@ export class TeamCollectionService {
|
|||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort Team Collections in a parent collection
|
||||
*
|
||||
* @param teamID The Team ID
|
||||
* @param parentID The Parent Collection ID
|
||||
* @param sortBy The sort option
|
||||
* @returns Boolean of sorting status
|
||||
*/
|
||||
async sortTeamCollections(
|
||||
teamID: string,
|
||||
parentID: string,
|
||||
sortBy: SortOptions,
|
||||
) {
|
||||
// Handle all sort options, including a default
|
||||
let orderBy: Prisma.Enumerable<Prisma.TeamCollectionOrderByWithRelationInput>;
|
||||
if (sortBy === SortOptions.TITLE_ASC) {
|
||||
orderBy = { title: 'asc' };
|
||||
} else if (sortBy === SortOptions.TITLE_DESC) {
|
||||
orderBy = { title: 'desc' };
|
||||
} else {
|
||||
orderBy = { orderIndex: 'asc' };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await this.prisma.acquireLocks(tx, 'TeamCollection', null, parentID);
|
||||
|
||||
const collections = await tx.teamCollection.findMany({
|
||||
where: { teamID, parentID },
|
||||
orderBy,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Update the orderIndex of each collection based on the new order
|
||||
const promises = collections.map((collection, i) =>
|
||||
tx.teamCollection.update({
|
||||
where: { id: collection.id },
|
||||
data: { orderIndex: i + 1 },
|
||||
}),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error from TeamCollectionService.sortTeamCollections',
|
||||
error,
|
||||
);
|
||||
return E.left(TEAM_COL_REORDERING_FAILED);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
TeamCollection as DbTeamCollection,
|
||||
} from '@prisma/client';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockTeamService = mockDeep<TeamService>();
|
||||
|
|
@ -711,6 +712,7 @@ describe('moveRequest', () => {
|
|||
).resolves.toEqualLeft(TEAM_REQ_REORDERING_FAILED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('totalRequestsInATeam', () => {
|
||||
test('should resolve right and return a total team reqs count ', async () => {
|
||||
mockPrisma.teamRequest.count.mockResolvedValueOnce(2);
|
||||
|
|
@ -732,13 +734,90 @@ describe('totalRequestsInATeam', () => {
|
|||
});
|
||||
expect(result).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamRequestsCount', () => {
|
||||
test('should return count of all Team Collections in the organization', async () => {
|
||||
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
||||
describe('getTeamRequestsCount', () => {
|
||||
test('should return count of all Team Collections in the organization', async () => {
|
||||
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
||||
|
||||
const result = await teamRequestService.getTeamRequestsCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
const result = await teamRequestService.getTeamRequestsCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortTeamRequests', () => {
|
||||
test('should resolve right if collectionID is null', async () => {
|
||||
const teamID = team.id;
|
||||
const result = await teamRequestService.sortTeamRequests(
|
||||
teamID,
|
||||
null,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.right(true));
|
||||
});
|
||||
|
||||
test('should resolve right and sorts team requests by TITLE_ASC', async () => {
|
||||
const teamID = team.id;
|
||||
const collectionID = teamCollection.id;
|
||||
|
||||
mockPrisma.$transaction.mockImplementation(async (cb) => cb(mockPrisma));
|
||||
mockPrisma.acquireLocks.mockResolvedValue(undefined);
|
||||
mockPrisma.teamRequest.findMany.mockResolvedValue(dbTeamRequests);
|
||||
|
||||
const result = await teamRequestService.sortTeamRequests(
|
||||
teamID,
|
||||
collectionID,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
|
||||
expect(result).toEqual(E.right(true));
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
expect(mockPrisma.teamRequest.findMany).toHaveBeenCalledWith({
|
||||
where: { teamID, collectionID },
|
||||
orderBy: { title: 'asc' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(mockPrisma.teamRequest.update).toHaveBeenCalledTimes(
|
||||
dbTeamRequests.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should resolve right and sorts team requests by TITLE_DESC', async () => {
|
||||
const teamID = team.id;
|
||||
const collectionID = teamCollection.id;
|
||||
|
||||
mockPrisma.$transaction.mockImplementation(async (cb) => cb(mockPrisma));
|
||||
mockPrisma.acquireLocks.mockResolvedValue(undefined);
|
||||
mockPrisma.teamRequest.findMany.mockResolvedValue(dbTeamRequests);
|
||||
|
||||
const result = await teamRequestService.sortTeamRequests(
|
||||
teamID,
|
||||
collectionID,
|
||||
SortOptions.TITLE_DESC,
|
||||
);
|
||||
|
||||
expect(result).toEqual(E.right(true));
|
||||
expect(mockPrisma.$transaction).toHaveBeenCalled();
|
||||
expect(mockPrisma.teamRequest.findMany).toHaveBeenCalledWith({
|
||||
where: { teamID, collectionID },
|
||||
orderBy: { title: 'desc' },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(mockPrisma.teamRequest.update).toHaveBeenCalledTimes(
|
||||
dbTeamRequests.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should returns left(TEAM_REQ_REORDERING_FAILED) on error', async () => {
|
||||
const teamID = team.id;
|
||||
const collectionID = teamCollection.id;
|
||||
|
||||
mockPrisma.$transaction.mockRejectedValue(new Error('fail'));
|
||||
const result = await teamRequestService.sortTeamRequests(
|
||||
teamID,
|
||||
collectionID,
|
||||
SortOptions.TITLE_ASC,
|
||||
);
|
||||
expect(result).toEqual(E.left(TEAM_REQ_REORDERING_FAILED));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { stringToJson } from 'src/utils';
|
|||
import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { Prisma, TeamRequest as DbTeamRequest } from '@prisma/client';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
@Injectable()
|
||||
export class TeamRequestService {
|
||||
|
|
@ -502,4 +503,57 @@ export class TeamRequestService {
|
|||
const teamRequestsCount = this.prisma.teamRequest.count();
|
||||
return teamRequestsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort Team Requests in a Collection based on the Sort Option
|
||||
*
|
||||
* @param teamID The Team ID
|
||||
* @param collectionID The Collection ID
|
||||
* @param sortOption The Sort Option
|
||||
* @returns An Either of a Boolean if the sorting operation was successful
|
||||
*/
|
||||
async sortTeamRequests(
|
||||
teamID: string,
|
||||
collectionID: string,
|
||||
sortBy: SortOptions,
|
||||
) {
|
||||
if (!collectionID) return E.right(true); // No sorting for requests in root collection
|
||||
|
||||
let orderBy: Prisma.Enumerable<Prisma.TeamRequestOrderByWithRelationInput>;
|
||||
if (sortBy === SortOptions.TITLE_ASC) {
|
||||
orderBy = { title: 'asc' };
|
||||
} else if (sortBy === SortOptions.TITLE_DESC) {
|
||||
orderBy = { title: 'desc' };
|
||||
} else {
|
||||
orderBy = { orderIndex: 'asc' };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// lock the rows
|
||||
await this.prisma.acquireLocks(tx, 'TeamRequest', null, null, [
|
||||
collectionID,
|
||||
]);
|
||||
const teamRequests = await tx.teamRequest.findMany({
|
||||
where: { teamID, collectionID },
|
||||
orderBy,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Update the orderIndex of each request based on the new order (parallel)
|
||||
const promises = teamRequests.map((request, i) =>
|
||||
tx.teamRequest.update({
|
||||
where: { id: request.id },
|
||||
data: { orderIndex: i + 1 },
|
||||
}),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error from TeamRequestService.sortTeamRequests', error);
|
||||
return E.left(TEAM_REQ_REORDERING_FAILED);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
packages/hoppscotch-backend/src/types/SortOptions.ts
Normal file
10
packages/hoppscotch-backend/src/types/SortOptions.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum SortOptions {
|
||||
TITLE_ASC = 'TITLE_ASC',
|
||||
TITLE_DESC = 'TITLE_DESC',
|
||||
}
|
||||
|
||||
registerEnumType(SortOptions, {
|
||||
name: 'SortOptions',
|
||||
});
|
||||
|
|
@ -35,6 +35,7 @@ import {
|
|||
} from 'src/utils';
|
||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||
import { PrismaError } from 'src/prisma/prisma-error-codes';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
@Injectable()
|
||||
export class UserCollectionService {
|
||||
|
|
@ -1312,4 +1313,54 @@ export class UserCollectionService {
|
|||
requests: transformedRequests,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort collections in a parent collection
|
||||
* @param userID The User UID
|
||||
* @param parentID The ID of the parent collection or null for root collections
|
||||
* @param sortBy The sorting option
|
||||
* @returns An Either of a Boolean if the sorting operation was successful
|
||||
*/
|
||||
async sortUserCollections(
|
||||
userID: string,
|
||||
parentID: string | null,
|
||||
sortBy: SortOptions,
|
||||
) {
|
||||
// Handle all sort options, including a default
|
||||
let orderBy: Prisma.Enumerable<Prisma.UserCollectionOrderByWithRelationInput>;
|
||||
if (sortBy === SortOptions.TITLE_ASC) {
|
||||
orderBy = { title: 'asc' };
|
||||
} else if (sortBy === SortOptions.TITLE_DESC) {
|
||||
orderBy = { title: 'desc' };
|
||||
} else {
|
||||
orderBy = { orderIndex: 'asc' };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await this.prisma.acquireLocks(tx, 'UserCollection', userID, parentID);
|
||||
|
||||
const collections = await tx.userCollection.findMany({
|
||||
where: { userUid: userID, parentID },
|
||||
orderBy,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const promises = collections.map((coll, index) =>
|
||||
tx.userCollection.update({
|
||||
where: { id: coll.id },
|
||||
data: { orderIndex: index + 1 },
|
||||
}),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error from UserCollectionService.sortUserCollections:', {
|
||||
error,
|
||||
});
|
||||
return E.left(USER_COLL_REORDERING_FAILED);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ import { UserRequestService } from './user-request.service';
|
|||
UserRequestUserCollectionResolver,
|
||||
UserRequestService,
|
||||
],
|
||||
exports: [UserRequestService],
|
||||
})
|
||||
export class UserRequestModule {}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { UserRequest } from './user-request.model';
|
||||
import { UserRequest as DbUserRequest } from '@prisma/client';
|
||||
import { Prisma, UserRequest as DbUserRequest } from '@prisma/client';
|
||||
import {
|
||||
USER_COLLECTION_NOT_FOUND,
|
||||
USER_REQUEST_CREATION_FAILED,
|
||||
|
|
@ -15,6 +15,7 @@ import { stringToJson } from 'src/utils';
|
|||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { ReqType } from 'src/types/RequestTypes';
|
||||
import { UserCollectionService } from 'src/user-collection/user-collection.service';
|
||||
import { SortOptions } from 'src/types/SortOptions';
|
||||
|
||||
@Injectable()
|
||||
export class UserRequestService {
|
||||
|
|
@ -486,4 +487,56 @@ export class UserRequestService {
|
|||
return E.left(USER_REQUEST_REORDERING_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort user requests inside a collection
|
||||
* @param userUid UID of the user who owns the collection
|
||||
* @param collectionID ID of the collection to which the requests belong
|
||||
* @param sortBy Sorting option
|
||||
* @returns Either of a boolean
|
||||
*/
|
||||
async sortUserRequests(
|
||||
userUid: string,
|
||||
collectionID: string,
|
||||
sortBy: SortOptions,
|
||||
): Promise<E.Left<string> | E.Right<boolean>> {
|
||||
if (!collectionID) return E.right(true);
|
||||
|
||||
let orderBy: Prisma.Enumerable<Prisma.UserRequestOrderByWithRelationInput>;
|
||||
if (sortBy === SortOptions.TITLE_ASC) {
|
||||
orderBy = { title: 'asc' };
|
||||
} else if (sortBy === SortOptions.TITLE_DESC) {
|
||||
orderBy = { title: 'desc' };
|
||||
} else {
|
||||
orderBy = { orderIndex: 'asc' };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await this.prisma.acquireLocks(tx, 'UserRequest', userUid, null, [
|
||||
collectionID,
|
||||
]);
|
||||
|
||||
const userRequests = await tx.userRequest.findMany({
|
||||
where: { userUid, collectionID },
|
||||
orderBy,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Update the orderIndex of each request based on the new order (parallel)
|
||||
const promises = userRequests.map((request, i) =>
|
||||
tx.userRequest.update({
|
||||
where: { id: request.id },
|
||||
data: { orderIndex: i + 1 },
|
||||
}),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error from UserRequestService.sortUserRequests', error);
|
||||
return E.left(USER_REQUEST_REORDERING_FAILED);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"send": "Send",
|
||||
"share": "Share",
|
||||
"show_secret": "Show secret",
|
||||
"sort": "Sort",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
|
|
@ -344,6 +345,7 @@
|
|||
"save_to_collection": "Save to Collection",
|
||||
"select": "Select a Collection",
|
||||
"select_location": "Select location",
|
||||
"sorted": "Collection sorted",
|
||||
"details": "Details",
|
||||
"duplicated": "Collection duplicated"
|
||||
},
|
||||
|
|
@ -604,7 +606,8 @@
|
|||
"name_length_insufficient": "Folder name should be at least 3 characters long",
|
||||
"new": "New Folder",
|
||||
"run": "Run Folder",
|
||||
"renamed": "Folder renamed"
|
||||
"renamed": "Folder renamed",
|
||||
"sorted": "Folder sorted"
|
||||
},
|
||||
"graphql": {
|
||||
"arguments": "Arguments",
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@
|
|||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div
|
||||
v-if="isCollectionLoading && !isOpen"
|
||||
class="flex items-center px-2"
|
||||
>
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<div v-else class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-if="!hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
|
|
@ -76,6 +82,7 @@
|
|||
@click="emit('add-folder')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!isEmpty"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconPlaySquare"
|
||||
:title="t('collection_runner.run_collection')"
|
||||
|
|
@ -108,6 +115,7 @@
|
|||
@keyup.x="exportAction?.$el.click()"
|
||||
@keyup.p="propertiesAction?.$el.click()"
|
||||
@keyup.t="runCollectionAction?.$el.click()"
|
||||
@keyup.s="sortAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
|
|
@ -137,6 +145,7 @@
|
|||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isEmpty"
|
||||
ref="runCollectionAction"
|
||||
:icon="IconPlaySquare"
|
||||
:label="t('collection_runner.run_collection')"
|
||||
|
|
@ -161,6 +170,19 @@
|
|||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!hasNoTeamAccess && isChildrenSortable"
|
||||
ref="sortAction"
|
||||
:icon="IconArrowUpDown"
|
||||
:label="t('action.sort')"
|
||||
:shortcut="['S']"
|
||||
@click="
|
||||
() => {
|
||||
sortCollection()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!hasNoTeamAccess"
|
||||
ref="duplicateAction"
|
||||
|
|
@ -201,6 +223,7 @@
|
|||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<HoppSmartItem
|
||||
v-if="!hasNoTeamAccess"
|
||||
ref="deleteAction"
|
||||
|
|
@ -261,6 +284,9 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
|
|||
import IconPlaySquare from "~icons/lucide/play-square"
|
||||
import IconSettings2 from "~icons/lucide/settings-2"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconArrowUpDown from "~icons/lucide/arrow-up-down"
|
||||
import { CurrentSortValuesService } from "~/services/current-sort.service"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
type CollectionType = "my-collections" | "team-collections"
|
||||
type FolderType = "collection" | "folder"
|
||||
|
|
@ -285,6 +311,7 @@ const props = withDefaults(
|
|||
collectionMoveLoading?: string[]
|
||||
isLastItem?: boolean
|
||||
duplicateCollectionLoading?: boolean
|
||||
teamLoadingCollections?: string[]
|
||||
}>(),
|
||||
{
|
||||
id: "",
|
||||
|
|
@ -296,7 +323,9 @@ const props = withDefaults(
|
|||
exportLoading: false,
|
||||
hasNoTeamAccess: false,
|
||||
isLastItem: false,
|
||||
duplicateLoading: false,
|
||||
duplicateCollectionLoading: false,
|
||||
collectionMoveLoading: () => [],
|
||||
teamLoadingCollections: () => [],
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -316,6 +345,14 @@ const emit = defineEmits<{
|
|||
(event: "update-collection-order", payload: DataTransfer): void
|
||||
(event: "update-last-collection-order", payload: DataTransfer): void
|
||||
(event: "run-collection", collectionID: string): void
|
||||
(
|
||||
event: "sort-collections",
|
||||
payload: {
|
||||
collectionID: string
|
||||
sortOrder: "asc" | "desc"
|
||||
collectionRefID: string
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<HTMLDivElement | null>(null)
|
||||
|
|
@ -328,18 +365,80 @@ const exportAction = ref<HTMLButtonElement | null>(null)
|
|||
const options = ref<TippyComponent | null>(null)
|
||||
const propertiesAction = ref<HTMLButtonElement | null>(null)
|
||||
const runCollectionAction = ref<HTMLButtonElement | null>(null)
|
||||
const sortAction = ref<HTMLButtonElement | null>(null)
|
||||
|
||||
const dragging = ref(false)
|
||||
const ordering = ref(false)
|
||||
const orderingLastItem = ref(false)
|
||||
const dropItemID = ref("")
|
||||
|
||||
/**
|
||||
* Determines if the collection/folder is empty.
|
||||
* A collection/folder is considered empty if it has no requests and no child folders.
|
||||
*/
|
||||
const isEmpty = computed(() => {
|
||||
if (!props.data) return true
|
||||
|
||||
if (props.collectionsType === "my-collections") {
|
||||
const collection = props.data as HoppCollection
|
||||
const req = collection.requests.length
|
||||
const fol = collection.folders.length
|
||||
|
||||
return req === 0 && fol === 0
|
||||
}
|
||||
|
||||
const teamCollection = props.data as TeamCollection
|
||||
const req = teamCollection.requests?.length ?? 0
|
||||
const child = teamCollection.children?.length ?? 0
|
||||
|
||||
return req === 0 && child === 0
|
||||
})
|
||||
|
||||
/**
|
||||
* Determines if the collection/folder is sortable.
|
||||
* A collection/folder is sortable if it has more than one request or more than one child folder.
|
||||
* or one request and one child folder.
|
||||
*/
|
||||
const isChildrenSortable = computed(() => {
|
||||
if (!props.data) return false
|
||||
|
||||
if (props.collectionsType === "my-collections") {
|
||||
const collection = props.data as HoppCollection
|
||||
const req = collection.requests.length
|
||||
const fol = collection.folders.length
|
||||
|
||||
return req > 1 || fol > 1 || (req === 1 && fol === 1)
|
||||
}
|
||||
|
||||
const teamCollection = props.data as TeamCollection
|
||||
const req = teamCollection.requests?.length ?? 0
|
||||
const child = teamCollection.children?.length ?? 0
|
||||
|
||||
return req > 1 || child > 1 || (req === 1 && child === 1)
|
||||
})
|
||||
|
||||
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
||||
type: "collection",
|
||||
id: "",
|
||||
parentID: "",
|
||||
})
|
||||
|
||||
const currentSortValuesService = useService(CurrentSortValuesService)
|
||||
|
||||
const collectionRefID = computed(() => {
|
||||
return props.collectionsType === "my-collections"
|
||||
? (props.data as HoppCollection)._ref_id
|
||||
: props.id
|
||||
})
|
||||
|
||||
const currentSortOrder = ref<"asc" | "desc">(
|
||||
currentSortValuesService.getSortOption(collectionRefID.value ?? "personal")
|
||||
?.sortOrder ?? "asc"
|
||||
)
|
||||
const isCollectionLoading = computed(() => {
|
||||
return props.teamLoadingCollections!.includes(props.id)
|
||||
})
|
||||
|
||||
// Used to determine if the collection is being dragged to a different destination
|
||||
// This is used to make the highlight effect work
|
||||
watch(
|
||||
|
|
@ -495,6 +594,16 @@ const isCollLoading = computed(() => {
|
|||
return false
|
||||
})
|
||||
|
||||
const sortCollection = () => {
|
||||
currentSortOrder.value = currentSortOrder.value === "asc" ? "desc" : "asc"
|
||||
|
||||
emit("sort-collections", {
|
||||
collectionID: props.id,
|
||||
sortOrder: currentSortOrder.value,
|
||||
collectionRefID: collectionRefID.value ?? "personal",
|
||||
})
|
||||
}
|
||||
|
||||
const resetDragState = () => {
|
||||
dragging.value = false
|
||||
ordering.value = false
|
||||
|
|
|
|||
|
|
@ -22,6 +22,14 @@
|
|||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="filteredCollections && filteredCollections.length > 1"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
blank
|
||||
:title="t('action.sort')"
|
||||
:icon="IconArrowUpDown"
|
||||
@click="debouncedSorting"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
|
|
@ -96,6 +104,7 @@
|
|||
emit('export-data', node.data.data.data)
|
||||
"
|
||||
@remove-collection="emit('remove-collection', node.id)"
|
||||
@sort-collections="emit('sort-collections', $event)"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
|
|
@ -184,7 +193,12 @@
|
|||
node.data.type === 'folders' &&
|
||||
emit('export-data', node.data.data.data)
|
||||
"
|
||||
@remove-collection="emit('remove-folder', node.id)"
|
||||
@remove-collection="
|
||||
node.data.type === 'folders' && emit('remove-folder', node.id)
|
||||
"
|
||||
@sort-collections="
|
||||
node.data.type === 'folders' && emit('sort-collections', $event)
|
||||
"
|
||||
@drop-event="dropEvent($event, node.id)"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
|
|
@ -225,7 +239,7 @@
|
|||
:is-active="
|
||||
isActiveRequest(
|
||||
node.data.data.parentIndex,
|
||||
parseInt(pathToIndex(node.id))
|
||||
node.data.data.data._ref_id ?? node.data.data.data.id
|
||||
)
|
||||
"
|
||||
:is-selected="
|
||||
|
|
@ -310,6 +324,8 @@
|
|||
dragRequest($event, {
|
||||
folderPath: node.data.data.parentIndex,
|
||||
requestIndex: node.id,
|
||||
requestRefID:
|
||||
node.data.data.data._ref_id ?? node.data.data.data.id,
|
||||
})
|
||||
"
|
||||
@update-request-order="
|
||||
|
|
@ -402,8 +418,9 @@
|
|||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import IconArrowUpDown from "~icons/lucide/arrow-up-down"
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, PropType, Ref, toRef } from "vue"
|
||||
import { computed, PropType, ref, Ref, toRef } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui/helpers"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
|
@ -413,6 +430,8 @@ import * as O from "fp-ts/Option"
|
|||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useDebounceFn } from "@vueuse/core"
|
||||
import { CurrentSortValuesService } from "~/services/current-sort.service"
|
||||
|
||||
export type Collection = {
|
||||
type: "collections"
|
||||
|
|
@ -577,6 +596,14 @@ const emit = defineEmits<{
|
|||
isActive: boolean
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "sort-collections",
|
||||
payload: {
|
||||
collectionID: string | null
|
||||
sortOrder: "asc" | "desc"
|
||||
collectionRefID: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "share-request",
|
||||
payload: {
|
||||
|
|
@ -589,6 +616,7 @@ const emit = defineEmits<{
|
|||
folderPath: string
|
||||
requestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
requestRefID?: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
|
|
@ -623,6 +651,12 @@ const emit = defineEmits<{
|
|||
|
||||
const refFilterCollection = toRef(props, "filteredCollections")
|
||||
|
||||
const currentSortValuesService = useService(CurrentSortValuesService)
|
||||
|
||||
const currentSortOrder = ref<"asc" | "desc">(
|
||||
currentSortValuesService.getSortOption("personal")?.sortOrder ?? "asc"
|
||||
)
|
||||
|
||||
const pathToIndex = (path: string) => {
|
||||
const pathArr = path.split("/")
|
||||
return pathArr[pathArr.length - 1]
|
||||
|
|
@ -659,9 +693,15 @@ const isSelected = ({
|
|||
}
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||
const active = computed(
|
||||
() =>
|
||||
tabs.currentActiveTab.value.document.type !== "test-runner" &&
|
||||
tabs.currentActiveTab.value.document.saveContext
|
||||
)
|
||||
|
||||
const isActiveRequest = (folderPath: string, requestRefID: string) => {
|
||||
if (active.value === null || !active.value) return false
|
||||
|
||||
const isActiveRequest = (folderPath: string, requestIndex: number) => {
|
||||
return pipe(
|
||||
active.value,
|
||||
O.fromNullable,
|
||||
|
|
@ -669,7 +709,7 @@ const isActiveRequest = (folderPath: string, requestIndex: number) => {
|
|||
(active) =>
|
||||
active.originLocation === "user-collection" &&
|
||||
active.folderPath === folderPath &&
|
||||
active.requestIndex === requestIndex &&
|
||||
active.requestRefID === requestRefID &&
|
||||
active.exampleID === undefined
|
||||
),
|
||||
O.isSome
|
||||
|
|
@ -694,7 +734,10 @@ const selectRequest = (data: {
|
|||
request,
|
||||
folderPath,
|
||||
requestIndex,
|
||||
isActive: isActiveRequest(folderPath, parseInt(requestIndex)),
|
||||
isActive: isActiveRequest(
|
||||
folderPath,
|
||||
request._ref_id ?? request.id ?? ""
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -708,11 +751,13 @@ const dragRequest = (
|
|||
{
|
||||
folderPath,
|
||||
requestIndex,
|
||||
}: { folderPath: string | null; requestIndex: string }
|
||||
requestRefID,
|
||||
}: { folderPath: string | null; requestIndex: string; requestRefID?: string }
|
||||
) => {
|
||||
if (!folderPath) return
|
||||
dataTransfer.setData("folderPath", folderPath)
|
||||
dataTransfer.setData("requestIndex", requestIndex)
|
||||
if (requestRefID) dataTransfer.setData("requestRefID", requestRefID)
|
||||
}
|
||||
|
||||
const dropEvent = (
|
||||
|
|
@ -722,12 +767,14 @@ const dropEvent = (
|
|||
const folderPath = dataTransfer.getData("folderPath")
|
||||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
const requestRefID = dataTransfer.getData("requestRefID")
|
||||
|
||||
if (folderPath && requestIndex) {
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
requestRefID,
|
||||
})
|
||||
} else {
|
||||
emit("drop-collection", {
|
||||
|
|
@ -771,6 +818,20 @@ const updateCollectionOrder = (
|
|||
})
|
||||
}
|
||||
|
||||
const debouncedSorting = useDebounceFn(() => {
|
||||
sortCollection()
|
||||
}, 250)
|
||||
|
||||
const sortCollection = () => {
|
||||
currentSortOrder.value = currentSortOrder.value === "asc" ? "desc" : "asc"
|
||||
|
||||
emit("sort-collections", {
|
||||
collectionID: null,
|
||||
sortOrder: currentSortOrder.value,
|
||||
collectionRefID: "personal",
|
||||
})
|
||||
}
|
||||
|
||||
type MyCollectionNode = Collection | Folder | Requests
|
||||
|
||||
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@
|
|||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="teamCollectionList && teamCollectionList.length > 1"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:disabled="
|
||||
(collectionsType.type === 'team-collections' &&
|
||||
collectionsType.selectedTeam === undefined) ||
|
||||
isShowingSearchResults ||
|
||||
hasNoTeamAccess
|
||||
"
|
||||
blank
|
||||
:title="t('action.sort')"
|
||||
:icon="IconArrowUpDown"
|
||||
@click="debouncedSorting"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
|
|
@ -40,8 +54,8 @@
|
|||
collectionsType.selectedTeam === undefined) ||
|
||||
isShowingSearchResults
|
||||
"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
:icon="IconImport"
|
||||
@click="emit('display-modal-import-export')"
|
||||
/>
|
||||
</span>
|
||||
|
|
@ -58,6 +72,7 @@
|
|||
:data="node.data.data.data"
|
||||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:team-loading-collections="teamLoadingCollections"
|
||||
:export-loading="exportLoading"
|
||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
|
|
@ -108,6 +123,7 @@
|
|||
emit('export-data', node.data.data.data)
|
||||
"
|
||||
@remove-collection="emit('remove-collection', node.id)"
|
||||
@sort-collections="emit('sort-collections', $event)"
|
||||
@drop-event="dropEvent($event, node.id, getPath(node.id, false))"
|
||||
@drag-event="dragEvent($event, node.id)"
|
||||
@update-collection-order="
|
||||
|
|
@ -128,12 +144,12 @@
|
|||
"
|
||||
@toggle-children="
|
||||
() => {
|
||||
;(toggleChildren(),
|
||||
saveRequest &&
|
||||
emit('select', {
|
||||
pickedType: 'teams-collection',
|
||||
collectionID: node.id,
|
||||
}))
|
||||
;(saveRequest &&
|
||||
emit('select', {
|
||||
pickedType: 'teams-collection',
|
||||
collectionID: node.id,
|
||||
}),
|
||||
toggleChildren())
|
||||
}
|
||||
"
|
||||
@run-collection="
|
||||
|
|
@ -159,6 +175,7 @@
|
|||
:collections-type="collectionsType.type"
|
||||
:is-open="isOpen"
|
||||
:export-loading="exportLoading"
|
||||
:team-loading-collections="teamLoadingCollections"
|
||||
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
|
||||
:collection-move-loading="collectionMoveLoading"
|
||||
:duplicate-collection-loading="duplicateCollectionLoading"
|
||||
|
|
@ -210,6 +227,9 @@
|
|||
node.data.type === 'folders' &&
|
||||
emit('remove-folder', node.data.data.data.id)
|
||||
"
|
||||
@sort-collections="
|
||||
node.data.type === 'folders' && emit('sort-collections', $event)
|
||||
"
|
||||
@drop-event="
|
||||
dropEvent($event, node.data.data.data.id, getPath(node.id, false))
|
||||
"
|
||||
|
|
@ -250,11 +270,12 @@
|
|||
"
|
||||
@click="
|
||||
() => {
|
||||
handleCollectionClick({
|
||||
// for the folders, we get a path, so we need to get the last part of the path which is the folder id
|
||||
collectionID: node.id.split('/').pop() as string,
|
||||
isOpen,
|
||||
})
|
||||
node.data.type === 'folders' &&
|
||||
handleCollectionClick({
|
||||
// for the folders, we get a path, so we need to get the last part of the path which is the folder id
|
||||
collectionID: node.id.split('/').pop() as string,
|
||||
isOpen,
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
|
@ -452,7 +473,9 @@
|
|||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import { computed, PropType, Ref, toRef } from "vue"
|
||||
import IconArrowUpDown from "~icons/lucide/arrow-up-down"
|
||||
|
||||
import { computed, PropType, ref, Ref, toRef } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
|
|
@ -466,6 +489,8 @@ import { Picked } from "~/helpers/types/HoppPicked.js"
|
|||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
import { TeamWorkspace } from "~/services/workspace.service"
|
||||
import { useDebounceFn } from "@vueuse/core"
|
||||
import { CurrentSortValuesService } from "~/services/current-sort.service"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
|
@ -631,12 +656,21 @@ const emit = defineEmits<{
|
|||
request: HoppRESTRequest
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "sort-collections",
|
||||
payload: {
|
||||
collectionID: string | null
|
||||
sortOrder: "asc" | "desc"
|
||||
collectionRefID: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-request",
|
||||
payload: {
|
||||
folderPath: string
|
||||
requestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
destinationParentPath?: string
|
||||
}
|
||||
): void
|
||||
(
|
||||
|
|
@ -684,6 +718,17 @@ const emit = defineEmits<{
|
|||
): void
|
||||
}>()
|
||||
|
||||
const currentSortValuesService = useService(CurrentSortValuesService)
|
||||
|
||||
const teamID = computed(() => {
|
||||
return props.collectionsType.selectedTeam?.teamID
|
||||
})
|
||||
|
||||
const currentSortOrder = ref<"asc" | "desc">(
|
||||
currentSortValuesService.getSortOption(teamID.value ?? "personal")
|
||||
?.sortOrder ?? "asc"
|
||||
)
|
||||
|
||||
const getPath = (path: string, pop: boolean = true) => {
|
||||
const pathArray = path.split("/")
|
||||
if (pop) pathArray.pop()
|
||||
|
|
@ -740,9 +785,14 @@ const isSelected = ({
|
|||
)
|
||||
}
|
||||
|
||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||
const active = computed(
|
||||
() =>
|
||||
tabs.currentActiveTab.value.document.type !== "test-runner" &&
|
||||
tabs.currentActiveTab.value.document.saveContext
|
||||
)
|
||||
|
||||
const isActiveRequest = (requestID: string) => {
|
||||
if (!active.value) return false
|
||||
return pipe(
|
||||
active.value,
|
||||
O.fromNullable,
|
||||
|
|
@ -807,12 +857,12 @@ const dropEvent = (
|
|||
const requestIndex = dataTransfer.getData("requestIndex")
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
const currentParentIndex = dataTransfer.getData("parentIndex")
|
||||
|
||||
if (folderPath && requestIndex) {
|
||||
emit("drop-request", {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
destinationParentPath,
|
||||
})
|
||||
} else {
|
||||
emit("drop-collection", {
|
||||
|
|
@ -858,6 +908,20 @@ const updateCollectionOrder = (
|
|||
})
|
||||
}
|
||||
|
||||
const debouncedSorting = useDebounceFn(() => {
|
||||
sortCollection()
|
||||
}, 250)
|
||||
|
||||
const sortCollection = () => {
|
||||
currentSortOrder.value = currentSortOrder.value === "asc" ? "desc" : "asc"
|
||||
|
||||
emit("sort-collections", {
|
||||
collectionID: null,
|
||||
sortOrder: currentSortOrder.value,
|
||||
collectionRefID: teamID.value ?? "personal",
|
||||
})
|
||||
}
|
||||
|
||||
type TeamCollections = {
|
||||
isLastItem: boolean
|
||||
type: "collections"
|
||||
|
|
@ -935,7 +999,7 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
|||
}
|
||||
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||
|
||||
!props.teamLoadingCollections.includes(parsedID) &&
|
||||
if (!props.teamLoadingCollections.includes(parsedID))
|
||||
emit("expand-team-collection", parsedID)
|
||||
|
||||
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const toast = useToast()
|
|||
const props = defineProps<{
|
||||
show: boolean
|
||||
folderPath?: string
|
||||
collectionIndex: number
|
||||
collectionIndex?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@
|
|||
@click="
|
||||
() => {
|
||||
emit('edit-properties', {
|
||||
collectionIndex: String(collectionIndex),
|
||||
collectionIndex: String(folderPath) ?? '0',
|
||||
collection: collection,
|
||||
})
|
||||
hide()
|
||||
|
|
@ -188,12 +188,7 @@
|
|||
@duplicate-collection="$emit('duplicate-collection', $event)"
|
||||
@edit-request="$emit('edit-request', $event)"
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@edit-properties="
|
||||
$emit('edit-properties', {
|
||||
collectionIndex: `${collectionIndex}/${String(index)}`,
|
||||
collection: folder,
|
||||
})
|
||||
"
|
||||
@edit-properties="$emit('edit-properties', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@select-request="$emit('select-request', $event)"
|
||||
@drop-request="$emit('drop-request', $event)"
|
||||
|
|
@ -275,6 +270,7 @@ const props = defineProps<{
|
|||
collectionIndex: number | null
|
||||
collection: HoppCollection
|
||||
isFiltered: boolean
|
||||
folderPath: string
|
||||
}>()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
|
|
|||
|
|
@ -146,8 +146,8 @@
|
|||
@click="
|
||||
() => {
|
||||
emit('edit-properties', {
|
||||
collectionIndex: collectionIndex,
|
||||
collection: collection,
|
||||
collectionIndex: folderPath,
|
||||
collection: folder,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
|
|
@ -182,12 +182,7 @@
|
|||
@duplicate-collection="emit('duplicate-collection', $event)"
|
||||
@edit-request="emit('edit-request', $event)"
|
||||
@duplicate-request="emit('duplicate-request', $event)"
|
||||
@edit-properties="
|
||||
emit('edit-properties', {
|
||||
collectionIndex: `${folderPath}/${String(subFolderIndex)}`,
|
||||
collection: subFolder,
|
||||
})
|
||||
"
|
||||
@edit-properties="emit('edit-properties', $event)"
|
||||
@select="emit('select', $event)"
|
||||
@select-request="$emit('select-request', $event)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
:key="`collection-${index}`"
|
||||
:picked="picked"
|
||||
:name="collection.name"
|
||||
:folder-path="String(index)"
|
||||
:collection-index="index"
|
||||
:collection="collection"
|
||||
:is-filtered="filterText.length > 0"
|
||||
|
|
@ -607,8 +608,7 @@ const editProperties = ({
|
|||
|
||||
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
|
||||
let inheritedProperties = undefined
|
||||
|
||||
if (parentIndex) {
|
||||
if (parentIndex && parentIndex !== "") {
|
||||
inheritedProperties = cascadeParentCollectionForProperties(
|
||||
parentIndex,
|
||||
"graphql"
|
||||
|
|
@ -633,6 +633,7 @@ const setCollectionProperties = async (newCollection: {
|
|||
const isValidToken = await handleTokenValidation()
|
||||
if (!isValidToken) return
|
||||
const { collection, path, isRootCollection } = newCollection
|
||||
|
||||
if (!collection) {
|
||||
return
|
||||
}
|
||||
|
|
@ -644,11 +645,7 @@ const setCollectionProperties = async (newCollection: {
|
|||
}
|
||||
|
||||
nextTick(() => {
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
path,
|
||||
cascadeParentCollectionForProperties(path, "graphql"),
|
||||
"graphql"
|
||||
)
|
||||
updateInheritedPropertiesForAffectedRequests(path, "graphql")
|
||||
})
|
||||
|
||||
displayModalEditProperties(false)
|
||||
|
|
|
|||
|
|
@ -65,19 +65,21 @@
|
|||
@select="selectPicked"
|
||||
@select-response="selectResponse"
|
||||
@select-request="selectRequest"
|
||||
@sort-collections="sortCollections"
|
||||
@update-request-order="updateRequestOrder"
|
||||
@update-collection-order="updateCollectionOrder"
|
||||
/>
|
||||
|
||||
<CollectionsTeamCollections
|
||||
v-else
|
||||
:collections-type="collectionsType"
|
||||
:team-collection-list="
|
||||
filterTexts.length > 0 ? teamsSearchResults : teamCollectionList
|
||||
filterTexts.length > 0 ? teamsSearchResults : teamCollections
|
||||
"
|
||||
:team-loading-collections="
|
||||
filterTexts.length > 0
|
||||
? collectionsBeingLoadedFromSearch
|
||||
: teamLoadingCollections
|
||||
: teamCollectionService.loadingCollections.value
|
||||
"
|
||||
:filter-text="filterTexts"
|
||||
:export-loading="exportLoading"
|
||||
|
|
@ -117,6 +119,7 @@
|
|||
"
|
||||
@share-request="shareRequest"
|
||||
@select-request="selectRequest"
|
||||
@sort-collections="sortCollections"
|
||||
@select-response="selectResponse"
|
||||
@select="selectPicked"
|
||||
@update-request-order="updateRequestOrder"
|
||||
|
|
@ -255,6 +258,7 @@ import {
|
|||
deleteCollection,
|
||||
duplicateTeamCollection,
|
||||
moveRESTTeamCollection,
|
||||
sortTeamCollections,
|
||||
updateOrderRESTTeamCollection,
|
||||
updateTeamCollection,
|
||||
} from "~/helpers/backend/mutations/TeamCollection"
|
||||
|
|
@ -277,7 +281,6 @@ import {
|
|||
resolveSaveContextOnRequestReorder,
|
||||
} from "~/helpers/collection/request"
|
||||
import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service"
|
||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||
|
|
@ -301,6 +304,8 @@ import {
|
|||
saveRESTRequestAs,
|
||||
updateRESTCollectionOrder,
|
||||
updateRESTRequestOrder,
|
||||
sortRESTCollection,
|
||||
sortRESTFolder,
|
||||
} from "~/newstore/collections"
|
||||
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
|
|
@ -317,6 +322,9 @@ import { CollectionRunnerData } from "../http/test/RunnerModal.vue"
|
|||
import { HoppCollectionVariable } from "@hoppscotch/data"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
import { CurrentValueService } from "~/services/current-environment-value.service"
|
||||
import { TeamCollectionsService } from "~/services/team-collection.service"
|
||||
import { SortOptions } from "~/helpers/backend/graphql"
|
||||
import { CurrentSortValuesService } from "~/services/current-sort.service"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
|
@ -396,21 +404,18 @@ const requestMoveLoading = ref<string[]>([])
|
|||
const secretEnvironmentService = useService(SecretEnvironmentService)
|
||||
const currentEnvironmentValueService = useService(CurrentValueService)
|
||||
|
||||
// Sorting service to get and set sort options for collections and folders
|
||||
const currentSortValuesService = useService(CurrentSortValuesService)
|
||||
|
||||
// TeamList-Adapter
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
|
||||
// Team Collection Adapter
|
||||
const teamCollectionAdapter = new TeamCollectionAdapter(null)
|
||||
const teamCollectionList = useReadonlyStream(
|
||||
teamCollectionAdapter.collections$,
|
||||
[]
|
||||
)
|
||||
const teamLoadingCollections = useReadonlyStream(
|
||||
teamCollectionAdapter.loadingCollections$,
|
||||
[]
|
||||
)
|
||||
// Team Collection Service
|
||||
const teamCollectionService = useService(TeamCollectionsService)
|
||||
const teamCollections = teamCollectionService.collections
|
||||
|
||||
const teamEnvironmentAdapter = new TeamEnvironmentAdapter(undefined)
|
||||
|
||||
const {
|
||||
|
|
@ -509,7 +514,6 @@ onMounted(async () => {
|
|||
const switchToMyCollections = () => {
|
||||
collectionsType.value.type = "my-collections"
|
||||
collectionsType.value.selectedTeam = undefined
|
||||
teamCollectionAdapter.changeTeamID(null)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -534,13 +538,12 @@ const expandTeamCollection = (collectionID: string) => {
|
|||
return
|
||||
}
|
||||
|
||||
teamCollectionAdapter.expandCollection(collectionID)
|
||||
teamCollectionService.expandCollection(collectionID)
|
||||
}
|
||||
|
||||
const updateSelectedTeam = (team: TeamWorkspace) => {
|
||||
if (team) {
|
||||
collectionsType.value.type = "team-collections"
|
||||
teamCollectionAdapter.changeTeamID(team.teamID)
|
||||
collectionsType.value.selectedTeam = team
|
||||
REMEMBERED_TEAM_ID.value = team.teamID
|
||||
emit("update-team", team)
|
||||
|
|
@ -881,6 +884,7 @@ const onAddRequest = async (requestName: string) => {
|
|||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: insertionIndex,
|
||||
requestRefID: newRequest._ref_id,
|
||||
},
|
||||
inheritedProperties: cascadeParentCollectionForProperties(path, "rest"),
|
||||
})
|
||||
|
|
@ -933,9 +937,10 @@ const onAddRequest = async (requestName: string) => {
|
|||
requestID: createRequestInCollection.id,
|
||||
collectionID: path,
|
||||
teamID: createRequestInCollection.collection.team.id,
|
||||
requestRefID: newRequest._ref_id,
|
||||
},
|
||||
inheritedProperties:
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(path),
|
||||
teamCollectionService.cascadeParentCollectionForProperties(path),
|
||||
})
|
||||
|
||||
modalLoadingState.value = false
|
||||
|
|
@ -1327,15 +1332,15 @@ const updateEditingResponse = (newName: string) => {
|
|||
possibleExampleActiveTab.value.document.response.name = newName
|
||||
|
||||
nextTick(() => {
|
||||
const doc = possibleExampleActiveTab.value.document
|
||||
if (doc.type === "example-response") {
|
||||
doc.isDirty = false
|
||||
doc.saveContext = {
|
||||
originLocation: "user-collection",
|
||||
folderPath: folderPath,
|
||||
requestIndex: requestIndex,
|
||||
exampleID: editingResponseID.value!,
|
||||
}
|
||||
if (possibleExampleActiveTab.value.document.type === "test-runner")
|
||||
return
|
||||
|
||||
possibleExampleActiveTab.value.document.isDirty = false
|
||||
possibleExampleActiveTab.value.document.saveContext = {
|
||||
originLocation: "user-collection",
|
||||
folderPath: folderPath,
|
||||
requestIndex: requestIndex,
|
||||
exampleID: editingResponseID.value!,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1396,14 +1401,13 @@ const updateEditingResponse = (newName: string) => {
|
|||
) {
|
||||
possibleActiveResponseTab.value.document.response.name = newName
|
||||
nextTick(() => {
|
||||
const doc = possibleActiveResponseTab.value.document
|
||||
if (doc.type === "example-response") {
|
||||
doc.isDirty = false
|
||||
doc.saveContext = {
|
||||
originLocation: "team-collection",
|
||||
requestID,
|
||||
exampleID: editingResponseID.value!,
|
||||
}
|
||||
if (possibleActiveResponseTab.value.document.type === "test-runner")
|
||||
return
|
||||
possibleActiveResponseTab.value.document.isDirty = false
|
||||
possibleActiveResponseTab.value.document.saveContext = {
|
||||
originLocation: "team-collection",
|
||||
requestID,
|
||||
exampleID: editingResponseID.value!,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -2051,7 +2055,7 @@ const selectRequest = (selectedRequest: {
|
|||
cascadeParentCollectionForPropertiesForSearchResults(collectionID)
|
||||
} else {
|
||||
inheritedProperties =
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(folderPath)
|
||||
teamCollectionService.cascadeParentCollectionForProperties(folderPath)
|
||||
}
|
||||
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
|
|
@ -2071,6 +2075,7 @@ const selectRequest = (selectedRequest: {
|
|||
requestID: requestIndex,
|
||||
collectionID: folderPath,
|
||||
exampleID: undefined,
|
||||
requestRefID: request.id,
|
||||
},
|
||||
inheritedProperties: inheritedProperties,
|
||||
})
|
||||
|
|
@ -2093,6 +2098,7 @@ const selectRequest = (selectedRequest: {
|
|||
originLocation: "user-collection",
|
||||
folderPath: folderPath!,
|
||||
requestIndex: parseInt(requestIndex),
|
||||
requestRefID: request._ref_id ?? request.id,
|
||||
},
|
||||
inheritedProperties: cascadeParentCollectionForProperties(
|
||||
folderPath,
|
||||
|
|
@ -2169,7 +2175,7 @@ const selectResponse = (payload: {
|
|||
exampleID: responseID,
|
||||
},
|
||||
inheritedProperties:
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(
|
||||
teamCollectionService.cascadeParentCollectionForProperties(
|
||||
folderPath
|
||||
),
|
||||
})
|
||||
|
|
@ -2195,8 +2201,16 @@ const dropRequest = async (payload: {
|
|||
folderPath?: string | undefined
|
||||
requestIndex: string
|
||||
destinationCollectionIndex: string
|
||||
destinationParentPath?: string
|
||||
requestRefID?: string
|
||||
}) => {
|
||||
const { folderPath, requestIndex, destinationCollectionIndex } = payload
|
||||
const {
|
||||
folderPath,
|
||||
requestIndex,
|
||||
destinationCollectionIndex,
|
||||
destinationParentPath,
|
||||
requestRefID,
|
||||
} = payload
|
||||
|
||||
if (!requestIndex || !destinationCollectionIndex || !folderPath) return
|
||||
|
||||
|
|
@ -2209,6 +2223,7 @@ const dropRequest = async (payload: {
|
|||
originLocation: "user-collection",
|
||||
folderPath,
|
||||
requestIndex: pathToLastIndex(requestIndex),
|
||||
requestRefID,
|
||||
})
|
||||
|
||||
// If there is a tab attached to this request, change save its save context
|
||||
|
|
@ -2220,6 +2235,7 @@ const dropRequest = async (payload: {
|
|||
myCollections.value,
|
||||
destinationCollectionIndex
|
||||
).length,
|
||||
requestRefID: possibleTab.value.document.request._ref_id,
|
||||
}
|
||||
|
||||
possibleTab.value.document.inheritedProperties =
|
||||
|
|
@ -2271,10 +2287,11 @@ const dropRequest = async (payload: {
|
|||
possibleTab.value.document.saveContext = {
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
collectionID: destinationParentPath ?? destinationCollectionIndex,
|
||||
}
|
||||
possibleTab.value.document.inheritedProperties =
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(
|
||||
destinationCollectionIndex
|
||||
teamCollectionService.cascadeParentCollectionForProperties(
|
||||
destinationParentPath ?? destinationCollectionIndex
|
||||
)
|
||||
}
|
||||
toast.success(`${t("request.moved")}`)
|
||||
|
|
@ -2407,16 +2424,7 @@ const dropCollection = async (payload: {
|
|||
newCollectionPath
|
||||
)
|
||||
|
||||
const inheritedProperty = cascadeParentCollectionForProperties(
|
||||
newCollectionPath,
|
||||
"rest"
|
||||
)
|
||||
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
newCollectionPath,
|
||||
inheritedProperty,
|
||||
"rest"
|
||||
)
|
||||
updateInheritedPropertiesForAffectedRequests(newCollectionPath, "rest")
|
||||
|
||||
draggingToRoot.value = false
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
|
|
@ -2444,22 +2452,16 @@ const dropCollection = async (payload: {
|
|||
1
|
||||
)
|
||||
|
||||
if (destinationParentPath && currentParentIndex) {
|
||||
if (destinationParentPath) {
|
||||
updateSaveContextForAffectedRequests(
|
||||
currentParentIndex,
|
||||
`${destinationParentPath}`
|
||||
)
|
||||
}
|
||||
|
||||
const inheritedProperty =
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(
|
||||
currentParentIndex || collectionIndexDragged,
|
||||
`${destinationParentPath}/${collectionIndexDragged}`
|
||||
)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
`${destinationParentPath}/${collectionIndexDragged}`,
|
||||
inheritedProperty,
|
||||
"rest"
|
||||
)
|
||||
}, 300)
|
||||
|
|
@ -2471,10 +2473,13 @@ const dropCollection = async (payload: {
|
|||
|
||||
/**
|
||||
* Checks if the collection is already in the root
|
||||
* @param id - path of the collection
|
||||
* @param id - path of the collection, null if it's in the root
|
||||
* @returns boolean - true if the collection is already in the root
|
||||
*/
|
||||
const isAlreadyInRoot = (id: string) => {
|
||||
const isAlreadyInRoot = (id: string | null) => {
|
||||
// If there is no id, it means the collection is in the root
|
||||
if (!id) return true
|
||||
|
||||
const indexPath = pathToIndex(id)
|
||||
return indexPath.length === 1
|
||||
}
|
||||
|
|
@ -2487,6 +2492,7 @@ const isAlreadyInRoot = (id: string) => {
|
|||
const dropToRoot = async ({ dataTransfer }: DragEvent) => {
|
||||
if (dataTransfer) {
|
||||
const collectionIndexDragged = dataTransfer.getData("collectionIndex")
|
||||
const parentIndex = dataTransfer.getData("parentIndex")
|
||||
if (!collectionIndexDragged) return
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
const isValidToken = await handleTokenValidation()
|
||||
|
|
@ -2505,14 +2511,8 @@ const dropToRoot = async ({ dataTransfer }: DragEvent) => {
|
|||
`${rootLength - 1}`
|
||||
)
|
||||
|
||||
const inheritedProperty = cascadeParentCollectionForProperties(
|
||||
`${rootLength - 1}`,
|
||||
"rest"
|
||||
)
|
||||
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
`${rootLength - 1}`,
|
||||
inheritedProperty,
|
||||
"rest"
|
||||
)
|
||||
}
|
||||
|
|
@ -2539,6 +2539,16 @@ const dropToRoot = async ({ dataTransfer }: DragEvent) => {
|
|||
collectionMoveLoading.value.indexOf(collectionIndexDragged),
|
||||
1
|
||||
)
|
||||
if (collectionIndexDragged && parentIndex) {
|
||||
updateSaveContextForAffectedRequests(parentIndex, null)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
`${collectionIndexDragged}`,
|
||||
"rest"
|
||||
)
|
||||
}, 300)
|
||||
toast.success(`${t("collection.moved")}`)
|
||||
}
|
||||
)
|
||||
|
|
@ -2921,7 +2931,7 @@ const editProperties = async (payload: {
|
|||
|
||||
if (parentIndex) {
|
||||
const { auth, headers, variables } =
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(parentIndex)
|
||||
teamCollectionService.cascadeParentCollectionForProperties(parentIndex)
|
||||
|
||||
inheritedProperties = {
|
||||
auth,
|
||||
|
|
@ -3040,15 +3050,8 @@ const setCollectionProperties = (newCollection: {
|
|||
editRESTFolder(path, collection)
|
||||
}
|
||||
|
||||
const inheritedProperty = cascadeParentCollectionForProperties(path, "rest")
|
||||
|
||||
nextTick(() => {
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
path,
|
||||
inheritedProperty,
|
||||
"rest",
|
||||
collection._ref_id ?? collectionId!
|
||||
)
|
||||
updateInheritedPropertiesForAffectedRequests(path, "rest")
|
||||
})
|
||||
toast.success(t("collection.properties_updated"))
|
||||
} else if (hasTeamWriteAccess.value && collectionId) {
|
||||
|
|
@ -3072,14 +3075,7 @@ const setCollectionProperties = (newCollection: {
|
|||
//This is a hack to update the inherited properties of the requests if there an tab opened
|
||||
// since it takes a little bit of time to update the collection tree
|
||||
setTimeout(() => {
|
||||
const inheritedProperty =
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(path)
|
||||
updateInheritedPropertiesForAffectedRequests(
|
||||
path,
|
||||
inheritedProperty,
|
||||
"rest",
|
||||
collectionId
|
||||
)
|
||||
updateInheritedPropertiesForAffectedRequests(path, "rest")
|
||||
}, 300)
|
||||
}
|
||||
|
||||
|
|
@ -3093,7 +3089,7 @@ const runCollectionHandler = (
|
|||
) => {
|
||||
if (payload.path && collectionsType.value.type === "team-collections") {
|
||||
const inheritedProperties =
|
||||
teamCollectionAdapter.cascadeParentCollectionForProperties(payload.path)
|
||||
teamCollectionService.cascadeParentCollectionForProperties(payload.path)
|
||||
|
||||
if (inheritedProperties) {
|
||||
collectionRunnerData.value = {
|
||||
|
|
@ -3111,6 +3107,51 @@ const runCollectionHandler = (
|
|||
showCollectionsRunnerModal.value = true
|
||||
}
|
||||
|
||||
const sortCollections = (payload: {
|
||||
collectionID: string | null
|
||||
sortOrder: "asc" | "desc"
|
||||
collectionRefID: string
|
||||
}) => {
|
||||
const { collectionID, sortOrder, collectionRefID } = payload
|
||||
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
const collectionIndex = collectionID ? parseInt(collectionID) : null
|
||||
|
||||
if (isAlreadyInRoot(collectionID)) {
|
||||
sortRESTCollection(collectionIndex, sortOrder)
|
||||
toast.success(t("collection.sorted"))
|
||||
} else {
|
||||
if (!collectionID) return
|
||||
|
||||
sortRESTFolder(collectionID, sortOrder)
|
||||
toast.success(t("folder.sorted"))
|
||||
}
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
pipe(
|
||||
sortTeamCollections(
|
||||
collectionsType.value.selectedTeam.teamID,
|
||||
collectionID,
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
toast.success(t("collection.sorted"))
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
// Set the sort option in the service to persist the sort option
|
||||
// when the user navigates away and comes back
|
||||
currentSortValuesService.setSortOption(collectionRefID, {
|
||||
sortBy: "name",
|
||||
sortOrder,
|
||||
})
|
||||
}
|
||||
|
||||
const resolveConfirmModal = (title: string | null) => {
|
||||
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
|
||||
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ import { useService } from "dioc/vue"
|
|||
import { pipe } from "fp-ts/lib/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { computed, nextTick, onMounted, ref } from "vue"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
|
|
@ -142,7 +141,6 @@ import {
|
|||
TestRunnerCollectionsAdapter,
|
||||
} from "~/helpers/runner/adapter"
|
||||
import { getErrorMessage } from "~/helpers/runner/collection-tree"
|
||||
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
|
||||
import { transformInheritedCollectionVariablesToAggregateEnv } from "~/helpers/utils/inheritedCollectionVarTransformer"
|
||||
import {
|
||||
getRESTCollectionByRefId,
|
||||
|
|
@ -151,6 +149,7 @@ import {
|
|||
} from "~/newstore/collections"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { TeamCollectionsService } from "~/services/team-collection.service"
|
||||
import {
|
||||
TestRunnerRequest,
|
||||
TestRunnerService,
|
||||
|
|
@ -161,11 +160,8 @@ const t = useI18n()
|
|||
const toast = useToast()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const teamCollectionAdapter = new TeamCollectionAdapter(null)
|
||||
const teamCollectionList = useReadonlyStream(
|
||||
teamCollectionAdapter.collections$,
|
||||
[]
|
||||
)
|
||||
const teamCollectionService = useService(TeamCollectionsService)
|
||||
const teamCollectionList = teamCollectionService.collections
|
||||
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppTestRunnerDocument> }>()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
mutation SortTeamCollections(
|
||||
$teamID: ID!
|
||||
$parentCollectionID: ID
|
||||
$sortOption: SortOptions!
|
||||
) {
|
||||
sortTeamCollections(
|
||||
teamID: $teamID
|
||||
parentCollectionID: $parentCollectionID
|
||||
sortOption: $sortOption
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
subscription TeamChildCollectionSorted($teamID: ID!) {
|
||||
teamChildCollectionsSorted(teamID: $teamID)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
subscription TeamRootCollectionsSorted($teamID: ID!) {
|
||||
teamRootCollectionsSorted(teamID: $teamID)
|
||||
}
|
||||
|
|
@ -21,6 +21,10 @@ import {
|
|||
RenameCollectionDocument,
|
||||
RenameCollectionMutation,
|
||||
RenameCollectionMutationVariables,
|
||||
SortOptions,
|
||||
SortTeamCollectionsDocument,
|
||||
SortTeamCollectionsMutation,
|
||||
SortTeamCollectionsMutationVariables,
|
||||
UpdateCollectionOrderDocument,
|
||||
UpdateCollectionOrderMutation,
|
||||
UpdateCollectionOrderMutationVariables,
|
||||
|
|
@ -152,3 +156,18 @@ export const duplicateTeamCollection = (collectionID: string) =>
|
|||
>(DuplicateTeamCollectionDocument, {
|
||||
collectionID,
|
||||
})
|
||||
|
||||
export const sortTeamCollections = (
|
||||
teamID: string,
|
||||
parentCollectionID: string | null,
|
||||
sortOption: SortOptions
|
||||
) =>
|
||||
runMutation<
|
||||
SortTeamCollectionsMutation,
|
||||
SortTeamCollectionsMutationVariables,
|
||||
""
|
||||
>(SortTeamCollectionsDocument, {
|
||||
teamID,
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,19 +5,13 @@ import { runGQLQuery } from "../backend/GQLClient"
|
|||
import * as E from "fp-ts/Either"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { TeamCollectionsService } from "~/services/team-collection.service"
|
||||
import { cascadeParentCollectionForProperties } from "~/newstore/collections"
|
||||
|
||||
/**
|
||||
* Resolve save context on reorder
|
||||
* @param payload
|
||||
* @param payload.lastIndex
|
||||
* @param payload.newIndex
|
||||
* @param folderPath
|
||||
* @param payload.length
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function resolveSaveContextOnCollectionReorder(
|
||||
payload: {
|
||||
lastIndex: number
|
||||
|
|
@ -62,6 +56,7 @@ export function resolveSaveContextOnCollectionReorder(
|
|||
const tabService = getService(RESTTabService)
|
||||
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
if (tab.document.type === "test-runner") return false
|
||||
return (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
affectedPaths.has(tab.document.saveContext.folderPath)
|
||||
|
|
@ -69,36 +64,54 @@ export function resolveSaveContextOnCollectionReorder(
|
|||
})
|
||||
|
||||
for (const tab of tabs) {
|
||||
if (tab.value.document.saveContext?.originLocation === "user-collection") {
|
||||
if (
|
||||
tab.value.document.type !== "test-runner" &&
|
||||
tab.value.document.saveContext?.originLocation === "user-collection"
|
||||
) {
|
||||
const newPath = affectedPaths.get(
|
||||
tab.value.document.saveContext?.folderPath
|
||||
tab.value.document.saveContext.folderPath
|
||||
)!
|
||||
tab.value.document.saveContext.folderPath = newPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to transform team collection IDs when folders move and trim leading slashes.
|
||||
* @param currentID Current collection ID
|
||||
* @param oldPath Old collection path
|
||||
* @param newPath New collection path
|
||||
* @returns Updated collection ID
|
||||
*/
|
||||
const updateCollectionIDPath = (
|
||||
currentID: string | undefined,
|
||||
oldPath: string,
|
||||
newPath: string | null
|
||||
): string | undefined => {
|
||||
if (!currentID) return currentID
|
||||
const replaced = currentID.replace(oldPath, newPath ?? "")
|
||||
return replaced.replace(/^\/+/, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last folder path from the given path.
|
||||
* @param path Path can be folder path or collection path
|
||||
* * @param path Path can be folder path or collection path
|
||||
* @returns Get the last folder path from the given path
|
||||
*/
|
||||
const getLastParentFolderPath = (path?: string) => {
|
||||
if (!path) return ""
|
||||
const pathArray = path.split("/")
|
||||
return pathArray.slice(pathArray.length - 1, pathArray.length).join("/")
|
||||
return pathArray[pathArray.length - 1] ?? ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve save context for affected requests on drop folder from one to another
|
||||
* @param oldFolderPath
|
||||
* @param newFolderPath
|
||||
* @returns
|
||||
* Resolve save context for affected requests on drop folder
|
||||
* @param oldFolderPath Old folder path
|
||||
* @param newFolderPath New folder path
|
||||
*/
|
||||
|
||||
export function updateSaveContextForAffectedRequests(
|
||||
oldFolderPath: string,
|
||||
newFolderPath: string
|
||||
newFolderPath: string | null
|
||||
) {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
|
|
@ -114,7 +127,11 @@ export function updateSaveContextForAffectedRequests(
|
|||
|
||||
for (const tab of tabs) {
|
||||
if (tab.value.document.type === "test-runner") return
|
||||
if (tab.value.document.saveContext?.originLocation === "user-collection") {
|
||||
|
||||
if (
|
||||
tab.value.document.saveContext?.originLocation === "user-collection" &&
|
||||
newFolderPath
|
||||
) {
|
||||
tab.value.document.saveContext = {
|
||||
...tab.value.document.saveContext,
|
||||
folderPath: tab.value.document.saveContext.folderPath.replace(
|
||||
|
|
@ -127,7 +144,8 @@ export function updateSaveContextForAffectedRequests(
|
|||
) {
|
||||
tab.value.document.saveContext = {
|
||||
...tab.value.document.saveContext,
|
||||
collectionID: tab.value.document.saveContext!.collectionID?.replace(
|
||||
collectionID: updateCollectionIDPath(
|
||||
tab.value.document.saveContext.collectionID,
|
||||
oldFolderPath,
|
||||
newFolderPath
|
||||
),
|
||||
|
|
@ -135,97 +153,14 @@ export function updateSaveContextForAffectedRequests(
|
|||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Used to check the new folder path is close to the save context folder path or not
|
||||
* @param folderPathCurrent The path saved as the inherited path in the inherited properties
|
||||
* @param newFolderPath The incoming path
|
||||
* @param saveContextPath The save context of the request
|
||||
* @returns The path which is close to saveContext.folderPath
|
||||
*/
|
||||
function folderPathCloseToSaveContext(
|
||||
folderPathCurrent: string | undefined,
|
||||
newFolderPath: string,
|
||||
saveContextPath: string
|
||||
) {
|
||||
if (!folderPathCurrent) return newFolderPath
|
||||
|
||||
const folderPathCurrentArray = folderPathCurrent.split("/")
|
||||
const newFolderPathArray = newFolderPath.split("/")
|
||||
const saveContextFolderPathArray = saveContextPath.split("/")
|
||||
|
||||
const folderPathCurrentMatch = folderPathCurrentArray.filter(
|
||||
(folder, i) => folder === saveContextFolderPathArray[i]
|
||||
).length
|
||||
|
||||
const newFolderPathMatch = newFolderPathArray.filter(
|
||||
(folder, i) => folder === saveContextFolderPathArray[i]
|
||||
).length
|
||||
|
||||
return folderPathCurrentMatch > newFolderPathMatch
|
||||
? folderPathCurrent
|
||||
: newFolderPath
|
||||
}
|
||||
|
||||
function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
|
||||
const keyMap: { [key: string]: number[] } = {} // Map to store array of indices for each key
|
||||
|
||||
// Populate keyMap with the indices of each key
|
||||
arr.forEach((item, index) => {
|
||||
const key = item.inheritedHeader.key
|
||||
if (!(key in keyMap)) {
|
||||
keyMap[key] = []
|
||||
}
|
||||
keyMap[key].push(index)
|
||||
})
|
||||
|
||||
// Create a new array containing only the last occurrence of each key
|
||||
const result = []
|
||||
for (const key in keyMap) {
|
||||
if (Object.prototype.hasOwnProperty.call(keyMap, key)) {
|
||||
const lastIndex = keyMap[key][keyMap[key].length - 1]
|
||||
result.push(arr[lastIndex])
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the result array based on the parentID
|
||||
result.sort((a, b) => a.parentID.localeCompare(b.parentID))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Order collection variables based on their parentPath and parentID
|
||||
* eg: path like 4/0/0 should come before 4/0/1 nad 4 should come before 4/0
|
||||
* @param vars Collection of variables to be ordered
|
||||
* @returns Ordered collection of variables
|
||||
*/
|
||||
const orderCollectionVariables = (
|
||||
vars: HoppInheritedProperty["variables"]
|
||||
): HoppInheritedProperty["variables"] => {
|
||||
return vars.sort((a, b) => {
|
||||
if (a.parentPath && b.parentPath) {
|
||||
return a.parentPath.localeCompare(b.parentPath)
|
||||
}
|
||||
|
||||
if (a.parentPath) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (b.parentPath) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return a.parentID.localeCompare(b.parentID)
|
||||
})
|
||||
}
|
||||
|
||||
export function updateInheritedPropertiesForAffectedRequests(
|
||||
path: string,
|
||||
inheritedProperties: HoppInheritedProperty,
|
||||
type: "rest" | "graphql",
|
||||
collectionId?: string
|
||||
type: "rest" | "graphql"
|
||||
) {
|
||||
const tabService =
|
||||
type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
|
||||
const teamCollectionService = getService(TeamCollectionsService)
|
||||
|
||||
const effectedTabs = tabService.getTabsRefTo((tab) => {
|
||||
if ("type" in tab.document && tab.document.type === "test-runner")
|
||||
|
|
@ -245,71 +180,33 @@ export function updateInheritedPropertiesForAffectedRequests(
|
|||
)
|
||||
})
|
||||
|
||||
effectedTabs.map((tab) => {
|
||||
effectedTabs.forEach((tab) => {
|
||||
if (
|
||||
"type" in tab.value.document &&
|
||||
tab.value.document.type === "test-runner"
|
||||
)
|
||||
return
|
||||
if (!("inheritedProperties" in tab.value.document)) return
|
||||
|
||||
const inheritedParentID =
|
||||
tab.value.document.inheritedProperties?.auth.parentID
|
||||
|
||||
const contextPath =
|
||||
tab.value.document.saveContext?.originLocation === "team-collection"
|
||||
? tab.value.document.saveContext.collectionID
|
||||
: tab.value.document.saveContext?.folderPath
|
||||
|
||||
const effectedPath = folderPathCloseToSaveContext(
|
||||
inheritedParentID,
|
||||
path,
|
||||
contextPath ?? ""
|
||||
)
|
||||
|
||||
if (effectedPath === path) {
|
||||
if (tab.value.document.inheritedProperties) {
|
||||
tab.value.document.inheritedProperties.auth = inheritedProperties.auth
|
||||
}
|
||||
}
|
||||
|
||||
if (tab.value.document.inheritedProperties?.headers) {
|
||||
// filter out the headers with the parentID not as the path
|
||||
const headers = tab.value.document.inheritedProperties.headers.filter(
|
||||
(header) => header.parentID !== path
|
||||
)
|
||||
|
||||
// filter out the headers with the parentID as the path in the inheritedProperties
|
||||
const inheritedHeaders = inheritedProperties.headers.filter(
|
||||
(header) =>
|
||||
path.startsWith(header.parentID ?? "") || header.parentID === path
|
||||
)
|
||||
|
||||
// merge the headers with the parentID as the path
|
||||
const mergedHeaders = removeDuplicatesAndKeepLast([
|
||||
...new Set([...inheritedHeaders, ...headers]),
|
||||
])
|
||||
|
||||
tab.value.document.inheritedProperties.headers = mergedHeaders
|
||||
}
|
||||
|
||||
if (tab.value.document.inheritedProperties?.variables && !collectionId) {
|
||||
tab.value.document.inheritedProperties.variables =
|
||||
inheritedProperties.variables
|
||||
} else if (
|
||||
tab.value.document.inheritedProperties?.variables &&
|
||||
collectionId
|
||||
if (
|
||||
tab.value.document.saveContext?.originLocation === "team-collection" &&
|
||||
tab.value.document.inheritedProperties
|
||||
) {
|
||||
const tabInheritedVariables =
|
||||
tab.value.document.inheritedProperties.variables.filter(
|
||||
(variable) => variable.parentID !== collectionId
|
||||
tab.value.document.inheritedProperties =
|
||||
teamCollectionService.cascadeParentCollectionForProperties(
|
||||
tab.value.document.saveContext.collectionID!
|
||||
)
|
||||
}
|
||||
|
||||
// filter out the variables with the parentID as the path in the inheritedProperties
|
||||
const inheritedVariables = inheritedProperties.variables.filter(
|
||||
(variable) => variable.parentID === collectionId
|
||||
)
|
||||
|
||||
const finalVariables = orderCollectionVariables([
|
||||
...new Set([...inheritedVariables, ...tabInheritedVariables]),
|
||||
])
|
||||
|
||||
tab.value.document.inheritedProperties.variables = finalVariables
|
||||
if (
|
||||
tab.value.document.saveContext?.originLocation === "user-collection" &&
|
||||
tab.value.document.inheritedProperties
|
||||
) {
|
||||
tab.value.document.inheritedProperties =
|
||||
cascadeParentCollectionForProperties(
|
||||
tab.value.document.saveContext.folderPath,
|
||||
type
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -317,6 +214,7 @@ export function updateInheritedPropertiesForAffectedRequests(
|
|||
function resetSaveContextForAffectedRequests(folderPath: string) {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
if (tab.document.type === "test-runner") return false
|
||||
return (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
tab.document.saveContext.folderPath.startsWith(folderPath)
|
||||
|
|
@ -324,6 +222,7 @@ function resetSaveContextForAffectedRequests(folderPath: string) {
|
|||
})
|
||||
|
||||
for (const tab of tabs) {
|
||||
if (tab.value.document.type === "test-runner") return
|
||||
tab.value.document.saveContext = null
|
||||
tab.value.document.isDirty = true
|
||||
|
||||
|
|
@ -331,8 +230,6 @@ function resetSaveContextForAffectedRequests(folderPath: string) {
|
|||
// since the request is deleted, we need to remove the saved responses as well
|
||||
tab.value.document.request.responses = {}
|
||||
}
|
||||
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -340,20 +237,19 @@ function resetSaveContextForAffectedRequests(folderPath: string) {
|
|||
* Reset save context to null if requests are deleted from the team collection or its folder
|
||||
* only runs when collection or folder is deleted
|
||||
*/
|
||||
|
||||
export async function resetTeamRequestsContext() {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
if (tab.document.type === "test-runner") return false
|
||||
return tab.document.saveContext?.originLocation === "team-collection"
|
||||
})
|
||||
|
||||
for (const tab of tabs) {
|
||||
if (tab.value.document.type === "test-runner") return
|
||||
if (tab.value.document.saveContext?.originLocation === "team-collection") {
|
||||
const data = await runGQLQuery({
|
||||
query: GetSingleRequestDocument,
|
||||
variables: {
|
||||
requestID: tab.value.document.saveContext?.requestID,
|
||||
},
|
||||
variables: { requestID: tab.value.document.saveContext.requestID },
|
||||
})
|
||||
|
||||
if (E.isRight(data) && data.right.request === null) {
|
||||
|
|
@ -377,12 +273,12 @@ export function getFoldersByPath(
|
|||
|
||||
// path will be like this "0/0/1" these are the indexes of the folders
|
||||
const pathArray = path.split("/").map((index) => parseInt(index))
|
||||
|
||||
let currentCollection = collections[pathArray[0]]
|
||||
|
||||
if (pathArray.length === 1) {
|
||||
return currentCollection.folders
|
||||
}
|
||||
|
||||
for (let i = 1; i < pathArray.length; i++) {
|
||||
const folder = currentCollection.folders[pathArray[i]]
|
||||
if (folder) currentCollection = folder
|
||||
|
|
|
|||
|
|
@ -1033,7 +1033,27 @@ data2: {"type":"test2","typeId":"123"}`,
|
|||
describe("Parse curl command to Hopp REST Request", () => {
|
||||
for (const [i, { command, response }] of samples.entries()) {
|
||||
test(`for sample #${i + 1}:\n\n${command}`, () => {
|
||||
expect(parseCurlToHoppRESTReq(command)).toEqual(response)
|
||||
const actual = parseCurlToHoppRESTReq(command)
|
||||
|
||||
/**
|
||||
* An object possibly carrying an internal reference id.
|
||||
* @typedef {object} RefIdCarrier
|
||||
* @property {unknown} [_ref_id]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {object} T
|
||||
* @param {T & RefIdCarrier} obj
|
||||
* @returns {Omit<T, "_ref_id">}
|
||||
*/
|
||||
const stripRefId = (obj) => {
|
||||
const clone = { ...obj }
|
||||
delete clone._ref_id
|
||||
return clone
|
||||
}
|
||||
|
||||
// Strip off _ref_id added by makeRESTRequest for equality check because it is generated randomly
|
||||
expect(stripRefId(actual)).toEqual(stripRefId(response))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,11 +22,15 @@ export type HoppRESTSaveContext =
|
|||
/**
|
||||
* Index to the request
|
||||
*/
|
||||
requestIndex: number
|
||||
requestIndex?: number
|
||||
/**
|
||||
* ID of the example response
|
||||
*/
|
||||
exampleID?: string
|
||||
/**
|
||||
* Reference ID of the request, if available
|
||||
*/
|
||||
requestRefID?: string
|
||||
}
|
||||
| {
|
||||
/**
|
||||
|
|
@ -49,6 +53,10 @@ export type HoppRESTSaveContext =
|
|||
* ID of the example response
|
||||
*/
|
||||
exampleID?: string
|
||||
/**
|
||||
* Reference ID of the request, if available
|
||||
*/
|
||||
requestRefID?: string
|
||||
}
|
||||
| null
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ export type HoppRequestSaveContext =
|
|||
* Current request
|
||||
*/
|
||||
req?: HoppRESTRequest
|
||||
/**
|
||||
* Reference ID of the request, if available
|
||||
*/
|
||||
requestRefID?: string
|
||||
}
|
||||
| {
|
||||
/**
|
||||
|
|
@ -46,4 +50,8 @@ export type HoppRequestSaveContext =
|
|||
* Current request
|
||||
*/
|
||||
req?: HoppRESTRequest
|
||||
/**
|
||||
* Reference ID of the request, if available
|
||||
*/
|
||||
requestRefID?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,6 +235,25 @@ function reorderItems(array: unknown[], from: number, to: number) {
|
|||
}
|
||||
}
|
||||
|
||||
function createComparator<T>(
|
||||
key: keyof T,
|
||||
sortOrder: "asc" | "desc" = "asc"
|
||||
): (a: T, b: T) => number {
|
||||
return (a, b) => {
|
||||
const aVal = a[key]
|
||||
const bVal = b[key]
|
||||
|
||||
if (typeof aVal === "string" && typeof bVal === "string") {
|
||||
return sortOrder === "asc"
|
||||
? aVal.localeCompare(bVal)
|
||||
: bVal.localeCompare(aVal)
|
||||
}
|
||||
|
||||
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1
|
||||
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1
|
||||
return 0
|
||||
}
|
||||
}
|
||||
const restCollectionDispatchers = defineDispatchers({
|
||||
setCollections(
|
||||
_: RESTCollectionStoreType,
|
||||
|
|
@ -298,6 +317,37 @@ const restCollectionDispatchers = defineDispatchers({
|
|||
}
|
||||
},
|
||||
|
||||
sortRESTCollection(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{
|
||||
collectionPath,
|
||||
sortOrder,
|
||||
}: { collectionPath: number | null; sortOrder: "asc" | "desc" }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
// If collectionPath is null, we are sorting the root collections
|
||||
if (collectionPath === null || isNaN(collectionPath)) {
|
||||
return {
|
||||
state: newState.sort(createComparator("name", sortOrder)),
|
||||
}
|
||||
}
|
||||
|
||||
const collection = newState.find((_, index) => index === collectionPath)
|
||||
|
||||
if (!collection) {
|
||||
console.error(`Collection not found.`)
|
||||
return {}
|
||||
}
|
||||
|
||||
collection.requests.sort(createComparator("name", sortOrder))
|
||||
collection.folders.sort(createComparator("name", sortOrder))
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
addFolder(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ name, path }: { name: string; path: string }
|
||||
|
|
@ -477,6 +527,40 @@ const restCollectionDispatchers = defineDispatchers({
|
|||
return { state: newState }
|
||||
},
|
||||
|
||||
sortRESTFolder(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{
|
||||
path,
|
||||
sortOrder,
|
||||
}: {
|
||||
path: string
|
||||
sortOrder: "asc" | "desc"
|
||||
}
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
if (indexPaths.length === 0) {
|
||||
console.log("Given path too short. Skipping request.")
|
||||
return {}
|
||||
}
|
||||
|
||||
const target = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
if (target === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring sortRESTFolder dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
target.requests.sort(createComparator("name", sortOrder))
|
||||
target.folders.sort(createComparator("name", sortOrder))
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
updateCollectionOrder(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{
|
||||
|
|
@ -1396,6 +1480,19 @@ export function editRESTCollection(
|
|||
})
|
||||
}
|
||||
|
||||
export function sortRESTCollection(
|
||||
collectionPath: number | null,
|
||||
sortOrder: "asc" | "desc"
|
||||
) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "sortRESTCollection",
|
||||
payload: {
|
||||
collectionPath,
|
||||
sortOrder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTFolder(name: string, path: string) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "addFolder",
|
||||
|
|
@ -1436,6 +1533,16 @@ export function moveRESTFolder(path: string, destinationPath: string | null) {
|
|||
})
|
||||
}
|
||||
|
||||
export function sortRESTFolder(path: string, sortOrder: "asc" | "desc") {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "sortRESTFolder",
|
||||
payload: {
|
||||
path,
|
||||
sortOrder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function duplicateRESTCollection(
|
||||
path: string,
|
||||
collectionSyncID?: string
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { TestContainer } from "dioc/testing"
|
||||
import {
|
||||
CurrentSortOption,
|
||||
CurrentSortValuesService,
|
||||
} from "../current-sort.service"
|
||||
|
||||
describe("CurrentSortValuesService", () => {
|
||||
let container: TestContainer
|
||||
let service: CurrentSortValuesService
|
||||
|
||||
beforeEach(() => {
|
||||
container = new TestContainer()
|
||||
service = container.bind(CurrentSortValuesService)
|
||||
})
|
||||
|
||||
describe("setSortOption & getSortOption", () => {
|
||||
it("should set and retrieve a sort option for a given ID", () => {
|
||||
const id = "col1"
|
||||
const option: CurrentSortOption = { sortBy: "name", sortOrder: "asc" }
|
||||
|
||||
service.setSortOption(id, option)
|
||||
expect(service.getSortOption(id)).toEqual(option)
|
||||
})
|
||||
|
||||
it("should return undefined for a non-existent ID", () => {
|
||||
expect(service.getSortOption("missing")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeSortOption", () => {
|
||||
it("should remove a sort option for a given ID", () => {
|
||||
const id = "col1"
|
||||
const option: CurrentSortOption = { sortBy: "name", sortOrder: "asc" }
|
||||
|
||||
service.setSortOption(id, option)
|
||||
service.removeSortOption(id)
|
||||
|
||||
expect(service.getSortOption(id)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("clearAllSortOptions", () => {
|
||||
it("should clear all sort options", () => {
|
||||
service.setSortOption("col1", { sortBy: "name", sortOrder: "asc" })
|
||||
service.setSortOption("col2", { sortBy: "name", sortOrder: "desc" })
|
||||
|
||||
service.clearAllSortOptions()
|
||||
|
||||
expect(service.currentSortOptions.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("loadCurrentSortValuesFromPersistedState", () => {
|
||||
it("should load sort options from persisted state", () => {
|
||||
const state = {
|
||||
col1: { sortBy: "name", sortOrder: "asc" } as CurrentSortOption,
|
||||
col2: { sortBy: "name", sortOrder: "desc" } as CurrentSortOption,
|
||||
}
|
||||
|
||||
service.loadCurrentSortValuesFromPersistedState(state)
|
||||
|
||||
expect(service.getSortOption("col1")).toEqual(state.col1)
|
||||
expect(service.getSortOption("col2")).toEqual(state.col2)
|
||||
})
|
||||
|
||||
it("should clear existing options before loading", () => {
|
||||
service.setSortOption("old", { sortBy: "name", sortOrder: "asc" })
|
||||
|
||||
const state = {
|
||||
new: { sortBy: "name", sortOrder: "desc" } as CurrentSortOption,
|
||||
}
|
||||
|
||||
service.loadCurrentSortValuesFromPersistedState(state)
|
||||
|
||||
expect(service.getSortOption("old")).toBeUndefined()
|
||||
expect(service.getSortOption("new")).toEqual(state.new)
|
||||
})
|
||||
})
|
||||
|
||||
describe("persistableCurrentSortValues", () => {
|
||||
it("should return a persistable object of current sort options", () => {
|
||||
const id = "col1"
|
||||
const option: CurrentSortOption = { sortBy: "name", sortOrder: "asc" }
|
||||
service.setSortOption(id, option)
|
||||
|
||||
expect(service.persistableCurrentSortValues.value).toEqual({
|
||||
[id]: option,
|
||||
})
|
||||
})
|
||||
|
||||
it("should return an empty object when no sort options exist", () => {
|
||||
expect(service.persistableCurrentSortValues.value).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -64,6 +64,7 @@ describe("WorkspaceService", () => {
|
|||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "before update",
|
||||
role: null,
|
||||
})
|
||||
|
||||
service.updateWorkspaceTeamName("test")
|
||||
|
|
@ -72,6 +73,7 @@ describe("WorkspaceService", () => {
|
|||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "test",
|
||||
role: null,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -100,12 +102,14 @@ describe("WorkspaceService", () => {
|
|||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "test",
|
||||
role: null,
|
||||
})
|
||||
|
||||
expect(service.currentWorkspace.value).toEqual({
|
||||
type: "team",
|
||||
teamID: "test",
|
||||
teamName: "test",
|
||||
role: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import { Service } from "dioc"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { computed, reactive } from "vue"
|
||||
|
||||
/**
|
||||
* Defines a sort option.
|
||||
* For now, we only support sorting by name, ascending or descending.
|
||||
* In the future, we can add more sort options like date created, date modified, etc.
|
||||
*/
|
||||
export type CurrentSortOption = {
|
||||
sortBy: "name"
|
||||
sortOrder: "asc" | "desc"
|
||||
}
|
||||
|
||||
/**
|
||||
* This service is used to store and manage current sort options of collections and folders.
|
||||
* This can be order by name, ascending, descending, etc.
|
||||
*/
|
||||
export class CurrentSortValuesService extends Service {
|
||||
public static readonly ID = "CURRENT_SORT_VALUES_SERVICE"
|
||||
|
||||
/**
|
||||
* Map of sort options for collections and folders.
|
||||
* Key is the ID of the collection or folder.
|
||||
* Value is the sort option.
|
||||
*/
|
||||
public currentSortOptions = reactive(new Map<string, CurrentSortOption>())
|
||||
|
||||
/**
|
||||
* Gets the current sort option for a given collection or folder ID.
|
||||
* @param id ID of the collection or folder.
|
||||
* @returns Current sort option for the given ID, or `undefined` if not found.
|
||||
*/
|
||||
public getSortOption(id: string): CurrentSortOption | undefined {
|
||||
return this.currentSortOptions.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current sort option for a given collection or folder ID.
|
||||
* @param id ID of the collection or folder.
|
||||
* @param sortOption Sort option to set.
|
||||
*/
|
||||
public setSortOption(id: string, sortOption: CurrentSortOption) {
|
||||
this.currentSortOptions.set(id, cloneDeep(sortOption))
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the current sort option for a given collection or folder ID.
|
||||
* @param id ID of the collection or folder.
|
||||
*/
|
||||
public removeSortOption(id: string) {
|
||||
this.currentSortOptions.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all sort options.
|
||||
* This is useful when the user logs out or switches accounts.
|
||||
* */
|
||||
public clearAllSortOptions() {
|
||||
this.currentSortOptions.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads current sort values from persisted state.
|
||||
* @param currentSortOptions Object containing current sort options to load.
|
||||
*/
|
||||
public loadCurrentSortValuesFromPersistedState(
|
||||
currentSortOptions: Record<string, CurrentSortOption>
|
||||
) {
|
||||
if (currentSortOptions) {
|
||||
this.clearAllSortOptions()
|
||||
|
||||
Object.entries(currentSortOptions).forEach(([id, sortOption]) => {
|
||||
this.setSortOption(id, sortOption)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current sort options in a format suitable for persistence.
|
||||
*/
|
||||
public persistableCurrentSortValues = computed(() => {
|
||||
const currentSortOptions: Record<string, CurrentSortOption> = {}
|
||||
this.currentSortOptions.forEach((option, id) => {
|
||||
currentSortOptions[id] = option
|
||||
})
|
||||
return currentSortOptions
|
||||
})
|
||||
}
|
||||
|
|
@ -240,6 +240,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRequestDocument> = {
|
|||
body: { contentType: null, body: null },
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
_ref_id: "req_ref_id",
|
||||
},
|
||||
isDirty: false,
|
||||
type: "request",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import { WSRequest$, setWSRequest } from "../../newstore/WebSocketSession"
|
|||
|
||||
import {
|
||||
CURRENT_ENVIRONMENT_VALUE_SCHEMA,
|
||||
CURRENT_SORT_VALUES_SCHEMA,
|
||||
ENVIRONMENTS_SCHEMA,
|
||||
GLOBAL_ENVIRONMENT_SCHEMA,
|
||||
GQL_COLLECTION_SCHEMA,
|
||||
|
|
@ -100,6 +101,10 @@ import {
|
|||
import { cloneDeep } from "lodash-es"
|
||||
import { fixBrokenRequestVersion } from "~/helpers/fixBrokenRequestVersion"
|
||||
import { fixBrokenEnvironmentVersion } from "~/helpers/fixBrokenEnvironmentVersion"
|
||||
import {
|
||||
CurrentSortOption,
|
||||
CurrentSortValuesService,
|
||||
} from "../current-sort.service"
|
||||
|
||||
export const STORE_NAMESPACE = "persistence.v1"
|
||||
|
||||
|
|
@ -122,6 +127,7 @@ export const STORE_KEYS = {
|
|||
GQL_TABS: "gqlTabs",
|
||||
SECRET_ENVIRONMENTS: "secretEnvironments",
|
||||
CURRENT_ENVIRONMENT_VALUE: "currentEnvironmentValue",
|
||||
CURRENT_SORT_VALUES: "currentSortValues",
|
||||
SCHEMA_VERSION: "schema_version",
|
||||
} as const
|
||||
|
||||
|
|
@ -187,6 +193,10 @@ export class PersistenceService extends Service {
|
|||
private readonly currentEnvironmentValueService =
|
||||
this.bind(CurrentValueService)
|
||||
|
||||
private readonly currentSortValuesService = this.bind(
|
||||
CurrentSortValuesService
|
||||
)
|
||||
|
||||
private showErrorToast(key: string) {
|
||||
const toast = useToast()
|
||||
toast.error(
|
||||
|
|
@ -698,6 +708,50 @@ export class PersistenceService extends Service {
|
|||
})
|
||||
}
|
||||
|
||||
private async setupCurrentSortValuesPersistence() {
|
||||
const loadResult = await Store.get<any>(
|
||||
STORE_NAMESPACE,
|
||||
STORE_KEYS.CURRENT_SORT_VALUES
|
||||
)
|
||||
|
||||
try {
|
||||
if (E.isRight(loadResult) && loadResult.right) {
|
||||
const result = CURRENT_SORT_VALUES_SCHEMA.safeParse(loadResult.right)
|
||||
|
||||
if (result.success) {
|
||||
this.currentSortValuesService.loadCurrentSortValuesFromPersistedState(
|
||||
result.data
|
||||
)
|
||||
} else {
|
||||
this.showErrorToast(STORE_KEYS.CURRENT_SORT_VALUES)
|
||||
await Store.set(
|
||||
STORE_NAMESPACE,
|
||||
`${STORE_KEYS.CURRENT_SORT_VALUES}-backup`,
|
||||
loadResult.right
|
||||
)
|
||||
console.error(
|
||||
`Failed parsing persisted CURRENT_SORT_VALUES:`,
|
||||
JSON.stringify(loadResult.right)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed parsing persisted CURRENT_SORT_VALUES:`, loadResult)
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
this.currentSortValuesService.persistableCurrentSortValues,
|
||||
async (newData: Record<string, CurrentSortOption>) => {
|
||||
await Store.set(
|
||||
STORE_NAMESPACE,
|
||||
STORE_KEYS.CURRENT_SORT_VALUES,
|
||||
newData
|
||||
)
|
||||
},
|
||||
{ debounce: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
private async setupWebsocketPersistence() {
|
||||
const loadResult = await Store.get<any>(
|
||||
STORE_NAMESPACE,
|
||||
|
|
@ -986,6 +1040,8 @@ export class PersistenceService extends Service {
|
|||
|
||||
this.setupSecretEnvironmentsPersistence(),
|
||||
this.setupCurrentEnvironmentValuePersistence(),
|
||||
|
||||
this.setupCurrentSortValuesPersistence(),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -404,6 +404,18 @@ export const CURRENT_ENVIRONMENT_VALUE_SCHEMA = z.union([
|
|||
),
|
||||
])
|
||||
|
||||
export const CURRENT_SORT_VALUES_SCHEMA = z.union([
|
||||
z.object({}).strict(),
|
||||
|
||||
z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
sortBy: z.enum(["name"]),
|
||||
sortOrder: z.enum(["asc", "desc"]),
|
||||
})
|
||||
),
|
||||
])
|
||||
|
||||
const HoppTestResultSchema = z
|
||||
.object({
|
||||
tests: z.array(HoppTestDataSchema),
|
||||
|
|
@ -511,8 +523,9 @@ const HoppRESTSaveContextSchema = z.nullable(
|
|||
.object({
|
||||
originLocation: z.literal("user-collection"),
|
||||
folderPath: z.string(),
|
||||
requestIndex: z.number(),
|
||||
requestIndex: z.optional(z.number()),
|
||||
exampleID: z.optional(z.string()),
|
||||
requestRefID: z.optional(z.string()),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
|
|
@ -522,6 +535,7 @@ const HoppRESTSaveContextSchema = z.nullable(
|
|||
teamID: z.optional(z.string()),
|
||||
collectionID: z.optional(z.string()),
|
||||
exampleID: z.optional(z.string()),
|
||||
requestRefID: z.optional(z.string()),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Container } from "dioc"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { computed } from "vue"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { HoppRESTSaveContext, HoppTabDocument } from "~/helpers/rest/document"
|
||||
|
|
@ -84,7 +83,13 @@ export class RESTTabService extends TabService<HoppTabDocument> {
|
|||
) {
|
||||
return this.getTabRef(tab.id)
|
||||
}
|
||||
} else if (isEqual(ctx, tab.document.saveContext)) {
|
||||
} else if (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
tab.document.saveContext.folderPath === ctx?.folderPath &&
|
||||
tab.document.saveContext.requestIndex === ctx?.requestIndex &&
|
||||
tab.document.saveContext.exampleID === ctx?.exampleID &&
|
||||
tab.document.saveContext.requestRefID === ctx?.requestRefID
|
||||
) {
|
||||
return this.getTabRef(tab.id)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1207
packages/hoppscotch-common/src/services/team-collection.service.ts
Normal file
1207
packages/hoppscotch-common/src/services/team-collection.service.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -24,7 +24,9 @@ import V13_VERSION from "./v/13"
|
|||
import { HoppRESTAuth } from "./v/15/auth"
|
||||
import V14_VERSION from "./v/14"
|
||||
import V15_VERSION from "./v/15/index"
|
||||
import V16_VERSION from "./v/16"
|
||||
import { HoppRESTRequestResponses } from "../rest-request-response"
|
||||
import { generateUniqueRefId } from "../utils/collection"
|
||||
|
||||
export * from "./content-types"
|
||||
|
||||
|
|
@ -73,7 +75,7 @@ const versionedObject = z.object({
|
|||
})
|
||||
|
||||
export const HoppRESTRequest = createVersionedEntity({
|
||||
latestVersion: 15,
|
||||
latestVersion: 16,
|
||||
versionMap: {
|
||||
0: V0_VERSION,
|
||||
1: V1_VERSION,
|
||||
|
|
@ -91,6 +93,7 @@ export const HoppRESTRequest = createVersionedEntity({
|
|||
13: V13_VERSION,
|
||||
14: V14_VERSION,
|
||||
15: V15_VERSION,
|
||||
16: V16_VERSION,
|
||||
},
|
||||
getVersion(data) {
|
||||
// For V1 onwards we have the v string storing the number
|
||||
|
|
@ -131,9 +134,10 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
|
|||
lodashIsEqualEq
|
||||
),
|
||||
responses: lodashIsEqualEq,
|
||||
_ref_id: undefinedEq(S.Eq),
|
||||
})
|
||||
|
||||
export const RESTReqSchemaVersion = "15"
|
||||
export const RESTReqSchemaVersion = "16"
|
||||
|
||||
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
||||
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
||||
|
|
@ -160,6 +164,8 @@ export function safelyExtractRESTRequest(
|
|||
if (!!x && typeof x === "object") {
|
||||
if ("id" in x && typeof x.id === "string") req.id = x.id
|
||||
|
||||
if ("_ref_id" in x && typeof x._ref_id === "string") req._ref_id = x._ref_id
|
||||
|
||||
if ("name" in x && typeof x.name === "string") req.name = x.name
|
||||
|
||||
if ("method" in x && typeof x.method === "string") req.method = x.method
|
||||
|
|
@ -185,7 +191,6 @@ export function safelyExtractRESTRequest(
|
|||
const result = HoppRESTAuth.safeParse(x.auth)
|
||||
|
||||
if (result.success) {
|
||||
// @ts-ignore
|
||||
req.auth = result.data
|
||||
}
|
||||
}
|
||||
|
|
@ -230,6 +235,7 @@ export function makeRESTRequest(
|
|||
): HoppRESTRequest {
|
||||
return {
|
||||
v: RESTReqSchemaVersion,
|
||||
_ref_id: x._ref_id ?? generateUniqueRefId("req"),
|
||||
...x,
|
||||
}
|
||||
}
|
||||
|
|
@ -254,6 +260,7 @@ export function getDefaultRESTRequest(): HoppRESTRequest {
|
|||
},
|
||||
requestVariables: [],
|
||||
responses: {},
|
||||
_ref_id: generateUniqueRefId("req"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
23
packages/hoppscotch-data/src/rest/v/16.ts
Normal file
23
packages/hoppscotch-data/src/rest/v/16.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { V15_SCHEMA } from "./15"
|
||||
import { z } from "zod"
|
||||
import { defineVersion } from "verzod"
|
||||
import { generateUniqueRefId } from "../../utils/collection"
|
||||
|
||||
export const V16_SCHEMA = V15_SCHEMA.extend({
|
||||
v: z.literal("16"),
|
||||
_ref_id: z.string().optional(),
|
||||
})
|
||||
|
||||
const V16_VERSION = defineVersion({
|
||||
schema: V16_SCHEMA,
|
||||
initial: false,
|
||||
up(old: z.infer<typeof V16_SCHEMA>) {
|
||||
return {
|
||||
...old,
|
||||
v: "16" as const,
|
||||
_ref_id: old._ref_id ?? generateUniqueRefId("req"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default V16_VERSION
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mutation DuplicateUserCollection($collectionID: String!, $reqType: ReqType!) {
|
||||
duplicateUserCollection(collectionID: $collectionID, reqType: $reqType)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
mutation SortUserCollections(
|
||||
$parentCollectionID: ID
|
||||
$sortOption: SortOptions!
|
||||
) {
|
||||
sortUserCollections(
|
||||
parentCollectionID: $parentCollectionID
|
||||
sortOption: $sortOption
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
subscription UserChildCollectionSorted {
|
||||
userChildCollectionsSorted {
|
||||
parentCollectionID
|
||||
sortOption
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
subscription UserRootCollectionsSorted {
|
||||
userRootCollectionsSorted {
|
||||
parentCollectionID
|
||||
sortOption
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,9 @@ import {
|
|||
DeleteUserRequestDocument,
|
||||
DeleteUserRequestMutation,
|
||||
DeleteUserRequestMutationVariables,
|
||||
DuplicateUserCollectionDocument,
|
||||
DuplicateUserCollectionMutation,
|
||||
DuplicateUserCollectionMutationVariables,
|
||||
ExportUserCollectionsToJsonDocument,
|
||||
ExportUserCollectionsToJsonQuery,
|
||||
ExportUserCollectionsToJsonQueryVariables,
|
||||
|
|
@ -50,6 +53,10 @@ import {
|
|||
RenameUserCollectionMutation,
|
||||
RenameUserCollectionMutationVariables,
|
||||
ReqType,
|
||||
SortOptions,
|
||||
SortUserCollectionsDocument,
|
||||
SortUserCollectionsMutation,
|
||||
SortUserCollectionsMutationVariables,
|
||||
UpdateGqlUserRequestDocument,
|
||||
UpdateGqlUserRequestMutation,
|
||||
UpdateGqlUserRequestMutationVariables,
|
||||
|
|
@ -62,6 +69,7 @@ import {
|
|||
UpdateUserCollectionOrderDocument,
|
||||
UpdateUserCollectionOrderMutation,
|
||||
UpdateUserCollectionOrderMutationVariables,
|
||||
UserChildCollectionSortedDocument,
|
||||
UserCollectionCreatedDocument,
|
||||
UserCollectionDuplicatedDocument,
|
||||
UserCollectionMovedDocument,
|
||||
|
|
@ -72,6 +80,7 @@ import {
|
|||
UserRequestDeletedDocument,
|
||||
UserRequestMovedDocument,
|
||||
UserRequestUpdatedDocument,
|
||||
UserRootCollectionsSortedDocument,
|
||||
} from "../../api/generated/graphql"
|
||||
|
||||
export const createRESTRootUserCollection = (title: string, data?: string) =>
|
||||
|
|
@ -197,6 +206,32 @@ export const moveUserCollection = (
|
|||
destCollectionID: destinationCollectionID,
|
||||
})()
|
||||
|
||||
export const duplicateUserCollection = (
|
||||
collectionID: string,
|
||||
reqType: ReqType
|
||||
) =>
|
||||
runMutation<
|
||||
DuplicateUserCollectionMutation,
|
||||
DuplicateUserCollectionMutationVariables,
|
||||
""
|
||||
>(DuplicateUserCollectionDocument, {
|
||||
collectionID,
|
||||
reqType,
|
||||
})()
|
||||
|
||||
export const sortUserCollections = (
|
||||
parentCollectionID: string | null,
|
||||
sortOption: SortOptions
|
||||
) =>
|
||||
runMutation<
|
||||
SortUserCollectionsMutation,
|
||||
SortUserCollectionsMutationVariables,
|
||||
""
|
||||
>(SortUserCollectionsDocument, {
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
})()
|
||||
|
||||
export const editUserRequest = (
|
||||
requestID: string,
|
||||
title: string,
|
||||
|
|
@ -337,6 +372,18 @@ export const runUserCollectionDuplicatedSubscription = () =>
|
|||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserRootCollectionsSortedSubscription = () =>
|
||||
runGQLSubscription({
|
||||
query: UserRootCollectionsSortedDocument,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserChildCollectionSortedSubscription = () =>
|
||||
runGQLSubscription({
|
||||
query: UserChildCollectionSortedDocument,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserRequestCreatedSubscription = () =>
|
||||
runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} })
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { runDispatchWithOutSyncing } from "../../lib/sync"
|
|||
|
||||
import {
|
||||
exportUserCollectionsToJSON,
|
||||
runUserChildCollectionSortedSubscription,
|
||||
runUserCollectionCreatedSubscription,
|
||||
runUserCollectionDuplicatedSubscription,
|
||||
runUserCollectionMovedSubscription,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
runUserRequestDeletedSubscription,
|
||||
runUserRequestMovedSubscription,
|
||||
runUserRequestUpdatedSubscription,
|
||||
runUserRootCollectionsSortedSubscription,
|
||||
} from "./collections.api"
|
||||
import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync"
|
||||
|
||||
|
|
@ -44,6 +46,8 @@ import {
|
|||
saveRESTRequestAs,
|
||||
setGraphqlCollections,
|
||||
setRESTCollections,
|
||||
sortRESTCollection,
|
||||
sortRESTFolder,
|
||||
updateRESTCollectionOrder,
|
||||
updateRESTRequestOrder,
|
||||
} from "@hoppscotch/common/newstore/collections"
|
||||
|
|
@ -280,6 +284,10 @@ function setupSubscriptions() {
|
|||
setupUserCollectionOrderUpdatedSubscription()
|
||||
const userCollectionDuplicatedSub =
|
||||
setupUserCollectionDuplicatedSubscription()
|
||||
const userRootCollectionsSortedSub =
|
||||
setupUserRootCollectionsSortedSubscription()
|
||||
const userChildCollectionSortedSub =
|
||||
setupUserChildCollectionSortedSubscription()
|
||||
|
||||
const userRequestCreatedSub = setupUserRequestCreatedSubscription()
|
||||
const userRequestUpdatedSub = setupUserRequestUpdatedSubscription()
|
||||
|
|
@ -293,6 +301,8 @@ function setupSubscriptions() {
|
|||
userCollectionMovedSub,
|
||||
userCollectionOrderUpdatedSub,
|
||||
userCollectionDuplicatedSub,
|
||||
userRootCollectionsSortedSub,
|
||||
userChildCollectionSortedSub,
|
||||
userRequestCreatedSub,
|
||||
userRequestUpdatedSub,
|
||||
userRequestDeletedSub,
|
||||
|
|
@ -711,6 +721,56 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
return userCollectionDuplicatedSub
|
||||
}
|
||||
|
||||
const setupUserRootCollectionsSortedSubscription = () => {
|
||||
const [userRootCollectionsSorted$, userRootCollectionsSortedSub] =
|
||||
runUserRootCollectionsSortedSubscription()
|
||||
|
||||
userRootCollectionsSorted$.subscribe((res) => {
|
||||
if (E.isRight(res)) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
if (res.right.userRootCollectionsSorted) {
|
||||
const { sortOption } = res.right.userRootCollectionsSorted
|
||||
|
||||
const sortOrder = sortOption === "TITLE_ASC" ? "asc" : "desc"
|
||||
|
||||
sortRESTCollection(null, sortOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return userRootCollectionsSortedSub
|
||||
}
|
||||
|
||||
const setupUserChildCollectionSortedSubscription = () => {
|
||||
const [userChildCollectionSorted$, userChildCollectionSortedSub] =
|
||||
runUserChildCollectionSortedSubscription()
|
||||
|
||||
userChildCollectionSorted$.subscribe((res) => {
|
||||
if (E.isRight(res)) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
if (res.right.userChildCollectionsSorted) {
|
||||
const { parentCollectionID, sortOption } =
|
||||
res.right.userChildCollectionsSorted
|
||||
|
||||
if (!parentCollectionID) return
|
||||
|
||||
const sortOrder = sortOption === "TITLE_ASC" ? "asc" : "desc"
|
||||
|
||||
const sourcePath = getCollectionPathFromCollectionID(
|
||||
parentCollectionID,
|
||||
restCollectionStore.value.state
|
||||
)
|
||||
|
||||
if (!sourcePath) return
|
||||
|
||||
sortRESTFolder(sourcePath, sortOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return userChildCollectionSortedSub
|
||||
}
|
||||
|
||||
function setupUserRequestCreatedSubscription() {
|
||||
const [userRequestCreated$, userRequestCreatedSub] =
|
||||
runUserRequestCreatedSubscription()
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ import {
|
|||
importUserCollectionsFromJSON,
|
||||
moveUserCollection,
|
||||
moveUserRequest,
|
||||
renameUserCollection,
|
||||
sortUserCollections,
|
||||
updateUserCollection,
|
||||
updateUserCollectionOrder,
|
||||
} from "./collections.api"
|
||||
|
||||
import { ReqType } from "../../api/generated/graphql"
|
||||
import { ReqType, SortOptions } from "../../api/generated/graphql"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
|
||||
|
|
@ -263,6 +263,34 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
|
||||
}
|
||||
},
|
||||
sortRESTCollection({ collectionPath, sortOrder }) {
|
||||
// If collectionPath is empty, it means we're sorting the whole root collections else we're sorting a specific collection
|
||||
const collectionID =
|
||||
collectionPath !== null && collectionPath !== undefined
|
||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||
collectionPath,
|
||||
])?.id
|
||||
: null
|
||||
|
||||
const order =
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
|
||||
sortUserCollections(collectionID ?? null, order)
|
||||
},
|
||||
|
||||
sortRESTFolder({ path, sortOrder }) {
|
||||
const collectionID = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
path.split("/").map((index) => parseInt(index))
|
||||
)?.id
|
||||
|
||||
if (collectionID) {
|
||||
const order =
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
|
||||
sortUserCollections(collectionID, order)
|
||||
}
|
||||
},
|
||||
async addFolder({ name, path }) {
|
||||
const parentCollection = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
mutation SortUserCollections(
|
||||
$parentCollectionID: ID
|
||||
$sortOption: SortOptions!
|
||||
) {
|
||||
sortUserCollections(
|
||||
parentCollectionID: $parentCollectionID
|
||||
sortOption: $sortOption
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
subscription UserChildCollectionSorted {
|
||||
userChildCollectionsSorted {
|
||||
parentCollectionID
|
||||
sortOption
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
subscription UserRootCollectionsSorted {
|
||||
userRootCollectionsSorted {
|
||||
parentCollectionID
|
||||
sortOption
|
||||
}
|
||||
}
|
||||
|
|
@ -38,16 +38,15 @@ export const getSyncInitFunction = <T extends DispatchingStore<any, any>>(
|
|||
let oldSyncStatus = shouldSyncValue()
|
||||
|
||||
// Start and stop the subscriptions according to the sync settings from profile
|
||||
shouldSyncObservable &&
|
||||
shouldSyncObservable.subscribe((newSyncStatus) => {
|
||||
if (oldSyncStatus === true && newSyncStatus === false) {
|
||||
stopListeningToSubscriptions()
|
||||
} else if (oldSyncStatus === false && newSyncStatus === true) {
|
||||
startListeningToSubscriptions()
|
||||
}
|
||||
shouldSyncObservable?.subscribe((newSyncStatus) => {
|
||||
if (oldSyncStatus && !newSyncStatus) {
|
||||
stopListeningToSubscriptions()
|
||||
} else if (newSyncStatus) {
|
||||
startListeningToSubscriptions()
|
||||
}
|
||||
|
||||
oldSyncStatus = newSyncStatus
|
||||
})
|
||||
oldSyncStatus = newSyncStatus
|
||||
})
|
||||
|
||||
function startStoreSync() {
|
||||
store.dispatches$.subscribe((actionParams) => {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ import {
|
|||
RenameUserCollectionMutation,
|
||||
RenameUserCollectionMutationVariables,
|
||||
ReqType,
|
||||
SortOptions,
|
||||
SortUserCollectionsDocument,
|
||||
SortUserCollectionsMutation,
|
||||
SortUserCollectionsMutationVariables,
|
||||
UpdateGqlUserRequestDocument,
|
||||
UpdateGqlUserRequestMutation,
|
||||
UpdateGqlUserRequestMutationVariables,
|
||||
|
|
@ -65,6 +69,7 @@ import {
|
|||
UpdateUserCollectionOrderDocument,
|
||||
UpdateUserCollectionOrderMutation,
|
||||
UpdateUserCollectionOrderMutationVariables,
|
||||
UserChildCollectionSortedDocument,
|
||||
UserCollectionCreatedDocument,
|
||||
UserCollectionDuplicatedDocument,
|
||||
UserCollectionMovedDocument,
|
||||
|
|
@ -75,6 +80,7 @@ import {
|
|||
UserRequestDeletedDocument,
|
||||
UserRequestMovedDocument,
|
||||
UserRequestUpdatedDocument,
|
||||
UserRootCollectionsSortedDocument,
|
||||
} from "@api/generated/graphql"
|
||||
|
||||
export const createRESTRootUserCollection = (title: string, data?: string) =>
|
||||
|
|
@ -213,6 +219,19 @@ export const duplicateUserCollection = (
|
|||
reqType,
|
||||
})()
|
||||
|
||||
export const sortUserCollections = (
|
||||
parentCollectionID: string | null,
|
||||
sortOption: SortOptions
|
||||
) =>
|
||||
runMutation<
|
||||
SortUserCollectionsMutation,
|
||||
SortUserCollectionsMutationVariables,
|
||||
""
|
||||
>(SortUserCollectionsDocument, {
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
})()
|
||||
|
||||
export const editUserRequest = (
|
||||
requestID: string,
|
||||
title: string,
|
||||
|
|
@ -353,6 +372,18 @@ export const runUserCollectionDuplicatedSubscription = () =>
|
|||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserRootCollectionsSortedSubscription = () =>
|
||||
runGQLSubscription({
|
||||
query: UserRootCollectionsSortedDocument,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserChildCollectionSortedSubscription = () =>
|
||||
runGQLSubscription({
|
||||
query: UserChildCollectionSortedDocument,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserRequestCreatedSubscription = () =>
|
||||
runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} })
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { runDispatchWithOutSyncing } from "@lib/sync"
|
|||
|
||||
import {
|
||||
exportUserCollectionsToJSON,
|
||||
runUserChildCollectionSortedSubscription,
|
||||
runUserCollectionCreatedSubscription,
|
||||
runUserCollectionDuplicatedSubscription,
|
||||
runUserCollectionMovedSubscription,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
runUserRequestDeletedSubscription,
|
||||
runUserRequestMovedSubscription,
|
||||
runUserRequestUpdatedSubscription,
|
||||
runUserRootCollectionsSortedSubscription,
|
||||
} from "./api"
|
||||
import { collectionsSyncer, getStoreByCollectionType } from "./sync"
|
||||
|
||||
|
|
@ -44,6 +46,8 @@ import {
|
|||
saveRESTRequestAs,
|
||||
setGraphqlCollections,
|
||||
setRESTCollections,
|
||||
sortRESTCollection,
|
||||
sortRESTFolder,
|
||||
updateRESTCollectionOrder,
|
||||
updateRESTRequestOrder,
|
||||
} from "@hoppscotch/common/newstore/collections"
|
||||
|
|
@ -288,6 +292,10 @@ function setupSubscriptions() {
|
|||
setupUserCollectionOrderUpdatedSubscription()
|
||||
const userCollectionDuplicatedSub =
|
||||
setupUserCollectionDuplicatedSubscription()
|
||||
const userRootCollectionsSortedSub =
|
||||
setupUserRootCollectionsSortedSubscription()
|
||||
const userChildCollectionSortedSub =
|
||||
setupUserChildCollectionSortedSubscription()
|
||||
|
||||
const userRequestCreatedSub = setupUserRequestCreatedSubscription()
|
||||
const userRequestUpdatedSub = setupUserRequestUpdatedSubscription()
|
||||
|
|
@ -301,6 +309,8 @@ function setupSubscriptions() {
|
|||
userCollectionMovedSub,
|
||||
userCollectionOrderUpdatedSub,
|
||||
userCollectionDuplicatedSub,
|
||||
userRootCollectionsSortedSub,
|
||||
userChildCollectionSortedSub,
|
||||
userRequestCreatedSub,
|
||||
userRequestUpdatedSub,
|
||||
userRequestDeletedSub,
|
||||
|
|
@ -725,6 +735,56 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
return userCollectionDuplicatedSub
|
||||
}
|
||||
|
||||
const setupUserRootCollectionsSortedSubscription = () => {
|
||||
const [userRootCollectionsSorted$, userRootCollectionsSortedSub] =
|
||||
runUserRootCollectionsSortedSubscription()
|
||||
|
||||
userRootCollectionsSorted$.subscribe((res) => {
|
||||
if (E.isRight(res)) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
if (res.right.userRootCollectionsSorted) {
|
||||
const { sortOption } = res.right.userRootCollectionsSorted
|
||||
|
||||
const sortOrder = sortOption === "TITLE_ASC" ? "asc" : "desc"
|
||||
|
||||
sortRESTCollection(null, sortOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return userRootCollectionsSortedSub
|
||||
}
|
||||
|
||||
const setupUserChildCollectionSortedSubscription = () => {
|
||||
const [userChildCollectionSorted$, userChildCollectionSortedSub] =
|
||||
runUserChildCollectionSortedSubscription()
|
||||
|
||||
userChildCollectionSorted$.subscribe((res) => {
|
||||
if (E.isRight(res)) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
if (res.right.userChildCollectionsSorted) {
|
||||
const { parentCollectionID, sortOption } =
|
||||
res.right.userChildCollectionsSorted
|
||||
|
||||
if (!parentCollectionID) return
|
||||
|
||||
const sortOrder = sortOption === "TITLE_ASC" ? "asc" : "desc"
|
||||
|
||||
const sourcePath = getCollectionPathFromCollectionID(
|
||||
parentCollectionID,
|
||||
restCollectionStore.value.state
|
||||
)
|
||||
|
||||
if (!sourcePath) return
|
||||
|
||||
sortRESTFolder(sourcePath, sortOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return userChildCollectionSortedSub
|
||||
}
|
||||
|
||||
function setupUserRequestCreatedSubscription() {
|
||||
const [userRequestCreated$, userRequestCreatedSub] =
|
||||
runUserRequestCreatedSubscription()
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ import {
|
|||
importUserCollectionsFromJSON,
|
||||
moveUserCollection,
|
||||
moveUserRequest,
|
||||
sortUserCollections,
|
||||
updateUserCollection,
|
||||
updateUserCollectionOrder,
|
||||
} from "./api"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ReqType } from "@api/generated/graphql"
|
||||
import { ReqType, SortOptions } from "@api/generated/graphql"
|
||||
|
||||
// restCollectionsMapper uses the collectionPath as the local identifier
|
||||
// Helper function to transform HoppCollection to backend format
|
||||
|
|
@ -267,6 +268,35 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
|
||||
}
|
||||
},
|
||||
|
||||
sortRESTCollection({ collectionPath, sortOrder }) {
|
||||
// If collectionPath is empty, it means we're sorting the whole root collections else we're sorting a specific collection
|
||||
const collectionID =
|
||||
collectionPath !== null && collectionPath !== undefined
|
||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||
collectionPath,
|
||||
])?.id
|
||||
: null
|
||||
|
||||
const order =
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
|
||||
sortUserCollections(collectionID ?? null, order)
|
||||
},
|
||||
|
||||
sortRESTFolder({ path, sortOrder }) {
|
||||
const collectionID = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
path.split("/").map((index) => parseInt(index))
|
||||
)?.id
|
||||
|
||||
if (collectionID) {
|
||||
const order =
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
|
||||
sortUserCollections(collectionID, order)
|
||||
}
|
||||
},
|
||||
async addFolder({ name, path }) {
|
||||
const parentCollection = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ import {
|
|||
RenameUserCollectionMutation,
|
||||
RenameUserCollectionMutationVariables,
|
||||
ReqType,
|
||||
SortOptions,
|
||||
SortUserCollectionsDocument,
|
||||
SortUserCollectionsMutation,
|
||||
SortUserCollectionsMutationVariables,
|
||||
UpdateGqlUserRequestDocument,
|
||||
UpdateGqlUserRequestMutation,
|
||||
UpdateGqlUserRequestMutationVariables,
|
||||
|
|
@ -65,6 +69,7 @@ import {
|
|||
UpdateUserCollectionOrderDocument,
|
||||
UpdateUserCollectionOrderMutation,
|
||||
UpdateUserCollectionOrderMutationVariables,
|
||||
UserChildCollectionSortedDocument,
|
||||
UserCollectionCreatedDocument,
|
||||
UserCollectionDuplicatedDocument,
|
||||
UserCollectionMovedDocument,
|
||||
|
|
@ -75,6 +80,7 @@ import {
|
|||
UserRequestDeletedDocument,
|
||||
UserRequestMovedDocument,
|
||||
UserRequestUpdatedDocument,
|
||||
UserRootCollectionsSortedDocument,
|
||||
} from "@api/generated/graphql"
|
||||
|
||||
export const createRESTRootUserCollection = (title: string, data?: string) =>
|
||||
|
|
@ -213,6 +219,19 @@ export const duplicateUserCollection = (
|
|||
reqType,
|
||||
})()
|
||||
|
||||
export const sortUserCollections = (
|
||||
parentCollectionID: string | null,
|
||||
sortOption: SortOptions
|
||||
) =>
|
||||
runMutation<
|
||||
SortUserCollectionsMutation,
|
||||
SortUserCollectionsMutationVariables,
|
||||
""
|
||||
>(SortUserCollectionsDocument, {
|
||||
parentCollectionID,
|
||||
sortOption,
|
||||
})()
|
||||
|
||||
export const editUserRequest = (
|
||||
requestID: string,
|
||||
title: string,
|
||||
|
|
@ -353,6 +372,18 @@ export const runUserCollectionDuplicatedSubscription = () =>
|
|||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserRootCollectionsSortedSubscription = () =>
|
||||
runGQLSubscription({
|
||||
query: UserRootCollectionsSortedDocument,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserChildCollectionSortedSubscription = () =>
|
||||
runGQLSubscription({
|
||||
query: UserChildCollectionSortedDocument,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
export const runUserRequestCreatedSubscription = () =>
|
||||
runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} })
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { runDispatchWithOutSyncing } from "@lib/sync"
|
|||
|
||||
import {
|
||||
exportUserCollectionsToJSON,
|
||||
runUserChildCollectionSortedSubscription,
|
||||
runUserCollectionCreatedSubscription,
|
||||
runUserCollectionDuplicatedSubscription,
|
||||
runUserCollectionMovedSubscription,
|
||||
|
|
@ -14,6 +15,7 @@ import {
|
|||
runUserRequestDeletedSubscription,
|
||||
runUserRequestMovedSubscription,
|
||||
runUserRequestUpdatedSubscription,
|
||||
runUserRootCollectionsSortedSubscription,
|
||||
} from "./api"
|
||||
import { collectionsSyncer, getStoreByCollectionType } from "./sync"
|
||||
|
||||
|
|
@ -44,6 +46,8 @@ import {
|
|||
saveRESTRequestAs,
|
||||
setGraphqlCollections,
|
||||
setRESTCollections,
|
||||
sortRESTCollection,
|
||||
sortRESTFolder,
|
||||
updateRESTCollectionOrder,
|
||||
updateRESTRequestOrder,
|
||||
} from "@hoppscotch/common/newstore/collections"
|
||||
|
|
@ -166,6 +170,7 @@ function exportedCollectionToHoppCollection(
|
|||
testScript,
|
||||
requestVariables,
|
||||
responses,
|
||||
_ref_id,
|
||||
} = request
|
||||
|
||||
const resolvedParams = addDescriptionField(params)
|
||||
|
|
@ -185,6 +190,7 @@ function exportedCollectionToHoppCollection(
|
|||
preRequestScript,
|
||||
testScript,
|
||||
responses,
|
||||
_ref_id: _ref_id ?? generateUniqueRefId("req"),
|
||||
}
|
||||
}),
|
||||
auth: data.auth,
|
||||
|
|
@ -288,6 +294,10 @@ function setupSubscriptions() {
|
|||
setupUserCollectionOrderUpdatedSubscription()
|
||||
const userCollectionDuplicatedSub =
|
||||
setupUserCollectionDuplicatedSubscription()
|
||||
const userRootCollectionsSortedSub =
|
||||
setupUserRootCollectionsSortedSubscription()
|
||||
const userChildCollectionSortedSub =
|
||||
setupUserChildCollectionSortedSubscription()
|
||||
|
||||
const userRequestCreatedSub = setupUserRequestCreatedSubscription()
|
||||
const userRequestUpdatedSub = setupUserRequestUpdatedSubscription()
|
||||
|
|
@ -301,6 +311,8 @@ function setupSubscriptions() {
|
|||
userCollectionMovedSub,
|
||||
userCollectionOrderUpdatedSub,
|
||||
userCollectionDuplicatedSub,
|
||||
userRootCollectionsSortedSub,
|
||||
userChildCollectionSortedSub,
|
||||
userRequestCreatedSub,
|
||||
userRequestUpdatedSub,
|
||||
userRequestDeletedSub,
|
||||
|
|
@ -725,6 +737,56 @@ function setupUserCollectionDuplicatedSubscription() {
|
|||
return userCollectionDuplicatedSub
|
||||
}
|
||||
|
||||
const setupUserRootCollectionsSortedSubscription = () => {
|
||||
const [userRootCollectionsSorted$, userRootCollectionsSortedSub] =
|
||||
runUserRootCollectionsSortedSubscription()
|
||||
|
||||
userRootCollectionsSorted$.subscribe((res) => {
|
||||
if (E.isRight(res)) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
if (res.right.userRootCollectionsSorted) {
|
||||
const { sortOption } = res.right.userRootCollectionsSorted
|
||||
|
||||
const sortOrder = sortOption === "TITLE_ASC" ? "asc" : "desc"
|
||||
|
||||
sortRESTCollection(null, sortOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return userRootCollectionsSortedSub
|
||||
}
|
||||
|
||||
const setupUserChildCollectionSortedSubscription = () => {
|
||||
const [userChildCollectionSorted$, userChildCollectionSortedSub] =
|
||||
runUserChildCollectionSortedSubscription()
|
||||
|
||||
userChildCollectionSorted$.subscribe((res) => {
|
||||
if (E.isRight(res)) {
|
||||
runDispatchWithOutSyncing(() => {
|
||||
if (res.right.userChildCollectionsSorted) {
|
||||
const { parentCollectionID, sortOption } =
|
||||
res.right.userChildCollectionsSorted
|
||||
|
||||
if (!parentCollectionID) return
|
||||
|
||||
const sortOrder = sortOption === "TITLE_ASC" ? "asc" : "desc"
|
||||
|
||||
const sourcePath = getCollectionPathFromCollectionID(
|
||||
parentCollectionID,
|
||||
restCollectionStore.value.state
|
||||
)
|
||||
|
||||
if (!sourcePath) return
|
||||
|
||||
sortRESTFolder(sourcePath, sortOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return userChildCollectionSortedSub
|
||||
}
|
||||
|
||||
function setupUserRequestCreatedSubscription() {
|
||||
const [userRequestCreated$, userRequestCreatedSub] =
|
||||
runUserRequestCreatedSubscription()
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ import {
|
|||
importUserCollectionsFromJSON,
|
||||
moveUserCollection,
|
||||
moveUserRequest,
|
||||
sortUserCollections,
|
||||
updateUserCollection,
|
||||
updateUserCollectionOrder,
|
||||
} from "./api"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ReqType } from "@api/generated/graphql"
|
||||
import { ReqType, SortOptions } from "@api/generated/graphql"
|
||||
|
||||
// restCollectionsMapper uses the collectionPath as the local identifier
|
||||
// Helper function to transform HoppCollection to backend format
|
||||
|
|
@ -270,6 +271,36 @@ export const storeSyncDefinition: StoreSyncDefinitionOf<
|
|||
updateUserCollection(collectionID, collection.name, JSON.stringify(data))
|
||||
}
|
||||
},
|
||||
|
||||
sortRESTCollection({ collectionPath, sortOrder }) {
|
||||
// If collectionPath is empty, it means we're sorting the whole root collections else we're sorting a specific collection
|
||||
const collectionID =
|
||||
collectionPath !== null && collectionPath !== undefined
|
||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||
collectionPath,
|
||||
])?.id
|
||||
: null
|
||||
|
||||
const order =
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
|
||||
sortUserCollections(collectionID ?? null, order)
|
||||
},
|
||||
|
||||
sortRESTFolder({ path, sortOrder }) {
|
||||
const collectionID = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
path.split("/").map((index) => parseInt(index))
|
||||
)?.id
|
||||
|
||||
if (collectionID) {
|
||||
const order =
|
||||
sortOrder === "asc" ? SortOptions.TitleAsc : SortOptions.TitleDesc
|
||||
|
||||
sortUserCollections(collectionID, order)
|
||||
}
|
||||
},
|
||||
|
||||
async addFolder({ name, path }) {
|
||||
const parentCollection = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
|
|
|
|||
Loading…
Reference in a new issue