Merge pull request #2707 from Podginator/feat/saved-links
Add Saved Searches, the ability to add new searches, delete searches, and reorder searches.
This commit is contained in:
@ -23,13 +23,13 @@ export class Filter {
|
||||
@Column('varchar', { length: 255 })
|
||||
name!: string
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true })
|
||||
@Column('varchar', { length: 255, nullable: true, default: null })
|
||||
description?: string | null
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
filter!: string
|
||||
|
||||
@Column('varchar', { length: 255 })
|
||||
@Column('varchar', { length: 255, default: 'Search' })
|
||||
category!: string
|
||||
|
||||
@Column('integer', { default: 0 })
|
||||
@ -40,4 +40,10 @@ export class Filter {
|
||||
|
||||
@UpdateDateColumn({ default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date
|
||||
|
||||
@Column('boolean', { default: false })
|
||||
defaultFilter!: boolean
|
||||
|
||||
@Column('boolean', { default: true })
|
||||
visible!: boolean
|
||||
}
|
||||
|
||||
@ -765,12 +765,14 @@ export type Filter = {
|
||||
__typename?: 'Filter';
|
||||
category: Scalars['String'];
|
||||
createdAt: Scalars['Date'];
|
||||
defaultFilter?: Maybe<Scalars['Boolean']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
filter: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
position: Scalars['Int'];
|
||||
updatedAt: Scalars['Date'];
|
||||
visible?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type FiltersError = {
|
||||
@ -1289,6 +1291,7 @@ export type Mutation = {
|
||||
setWebhook: SetWebhookResult;
|
||||
subscribe: SubscribeResult;
|
||||
unsubscribe: UnsubscribeResult;
|
||||
updateFilter: UpdateFilterResult;
|
||||
updateHighlight: UpdateHighlightResult;
|
||||
updateHighlightReply: UpdateHighlightReplyResult;
|
||||
updateLabel: UpdateLabelResult;
|
||||
@ -1589,6 +1592,11 @@ export type MutationUnsubscribeArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateFilterArgs = {
|
||||
input: UpdateFilterInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateHighlightArgs = {
|
||||
input: UpdateHighlightInput;
|
||||
};
|
||||
@ -2248,11 +2256,11 @@ export enum SaveFilterErrorCode {
|
||||
}
|
||||
|
||||
export type SaveFilterInput = {
|
||||
category: Scalars['String'];
|
||||
category?: InputMaybe<Scalars['String']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
filter: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
name: Scalars['String'];
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
export type SaveFilterResult = SaveFilterError | SaveFilterSuccess;
|
||||
@ -2837,6 +2845,34 @@ export type UnsubscribeSuccess = {
|
||||
subscription: Subscription;
|
||||
};
|
||||
|
||||
export type UpdateFilterError = {
|
||||
__typename?: 'UpdateFilterError';
|
||||
errorCodes: Array<UpdateFilterErrorCode>;
|
||||
};
|
||||
|
||||
export enum UpdateFilterErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type UpdateFilterInput = {
|
||||
category?: InputMaybe<Scalars['String']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
filter?: InputMaybe<Scalars['String']>;
|
||||
id: Scalars['String'];
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
position?: InputMaybe<Scalars['Int']>;
|
||||
visible?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type UpdateFilterResult = UpdateFilterError | UpdateFilterSuccess;
|
||||
|
||||
export type UpdateFilterSuccess = {
|
||||
__typename?: 'UpdateFilterSuccess';
|
||||
filter: Filter;
|
||||
};
|
||||
|
||||
export type UpdateHighlightError = {
|
||||
__typename?: 'UpdateHighlightError';
|
||||
errorCodes: Array<UpdateHighlightErrorCode>;
|
||||
@ -3777,6 +3813,11 @@ export type ResolversTypes = {
|
||||
UnsubscribeErrorCode: UnsubscribeErrorCode;
|
||||
UnsubscribeResult: ResolversTypes['UnsubscribeError'] | ResolversTypes['UnsubscribeSuccess'];
|
||||
UnsubscribeSuccess: ResolverTypeWrapper<UnsubscribeSuccess>;
|
||||
UpdateFilterError: ResolverTypeWrapper<UpdateFilterError>;
|
||||
UpdateFilterErrorCode: UpdateFilterErrorCode;
|
||||
UpdateFilterInput: UpdateFilterInput;
|
||||
UpdateFilterResult: ResolversTypes['UpdateFilterError'] | ResolversTypes['UpdateFilterSuccess'];
|
||||
UpdateFilterSuccess: ResolverTypeWrapper<UpdateFilterSuccess>;
|
||||
UpdateHighlightError: ResolverTypeWrapper<UpdateHighlightError>;
|
||||
UpdateHighlightErrorCode: UpdateHighlightErrorCode;
|
||||
UpdateHighlightInput: UpdateHighlightInput;
|
||||
@ -4185,6 +4226,10 @@ export type ResolversParentTypes = {
|
||||
UnsubscribeError: UnsubscribeError;
|
||||
UnsubscribeResult: ResolversParentTypes['UnsubscribeError'] | ResolversParentTypes['UnsubscribeSuccess'];
|
||||
UnsubscribeSuccess: UnsubscribeSuccess;
|
||||
UpdateFilterError: UpdateFilterError;
|
||||
UpdateFilterInput: UpdateFilterInput;
|
||||
UpdateFilterResult: ResolversParentTypes['UpdateFilterError'] | ResolversParentTypes['UpdateFilterSuccess'];
|
||||
UpdateFilterSuccess: UpdateFilterSuccess;
|
||||
UpdateHighlightError: UpdateHighlightError;
|
||||
UpdateHighlightInput: UpdateHighlightInput;
|
||||
UpdateHighlightReplyError: UpdateHighlightReplyError;
|
||||
@ -4793,12 +4838,14 @@ export type FeedArticlesSuccessResolvers<ContextType = ResolverContext, ParentTy
|
||||
export type FilterResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Filter'] = ResolversParentTypes['Filter']> = {
|
||||
category?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
defaultFilter?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
filter?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
position?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
visible?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
@ -5200,6 +5247,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
||||
setWebhook?: Resolver<ResolversTypes['SetWebhookResult'], ParentType, ContextType, RequireFields<MutationSetWebhookArgs, 'input'>>;
|
||||
subscribe?: Resolver<ResolversTypes['SubscribeResult'], ParentType, ContextType, RequireFields<MutationSubscribeArgs, 'input'>>;
|
||||
unsubscribe?: Resolver<ResolversTypes['UnsubscribeResult'], ParentType, ContextType, RequireFields<MutationUnsubscribeArgs, 'name'>>;
|
||||
updateFilter?: Resolver<ResolversTypes['UpdateFilterResult'], ParentType, ContextType, RequireFields<MutationUpdateFilterArgs, 'input'>>;
|
||||
updateHighlight?: Resolver<ResolversTypes['UpdateHighlightResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightArgs, 'input'>>;
|
||||
updateHighlightReply?: Resolver<ResolversTypes['UpdateHighlightReplyResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightReplyArgs, 'input'>>;
|
||||
updateLabel?: Resolver<ResolversTypes['UpdateLabelResult'], ParentType, ContextType, RequireFields<MutationUpdateLabelArgs, 'input'>>;
|
||||
@ -5907,6 +5955,20 @@ export type UnsubscribeSuccessResolvers<ContextType = ResolverContext, ParentTyp
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateFilterErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterError'] = ResolversParentTypes['UpdateFilterError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdateFilterErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateFilterResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterResult'] = ResolversParentTypes['UpdateFilterResult']> = {
|
||||
__resolveType: TypeResolveFn<'UpdateFilterError' | 'UpdateFilterSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateFilterSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateFilterSuccess'] = ResolversParentTypes['UpdateFilterSuccess']> = {
|
||||
filter?: Resolver<ResolversTypes['Filter'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateHighlightErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateHighlightError'] = ResolversParentTypes['UpdateHighlightError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdateHighlightErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@ -6466,6 +6528,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
||||
UnsubscribeError?: UnsubscribeErrorResolvers<ContextType>;
|
||||
UnsubscribeResult?: UnsubscribeResultResolvers<ContextType>;
|
||||
UnsubscribeSuccess?: UnsubscribeSuccessResolvers<ContextType>;
|
||||
UpdateFilterError?: UpdateFilterErrorResolvers<ContextType>;
|
||||
UpdateFilterResult?: UpdateFilterResultResolvers<ContextType>;
|
||||
UpdateFilterSuccess?: UpdateFilterSuccessResolvers<ContextType>;
|
||||
UpdateHighlightError?: UpdateHighlightErrorResolvers<ContextType>;
|
||||
UpdateHighlightReplyError?: UpdateHighlightReplyErrorResolvers<ContextType>;
|
||||
UpdateHighlightReplyResult?: UpdateHighlightReplyResultResolvers<ContextType>;
|
||||
|
||||
@ -677,12 +677,14 @@ type FeedArticlesSuccess {
|
||||
type Filter {
|
||||
category: String!
|
||||
createdAt: Date!
|
||||
defaultFilter: Boolean
|
||||
description: String
|
||||
filter: String!
|
||||
id: ID!
|
||||
name: String!
|
||||
position: Int!
|
||||
updatedAt: Date!
|
||||
visible: Boolean
|
||||
}
|
||||
|
||||
type FiltersError {
|
||||
@ -1157,6 +1159,7 @@ type Mutation {
|
||||
setWebhook(input: SetWebhookInput!): SetWebhookResult!
|
||||
subscribe(input: SubscribeInput!): SubscribeResult!
|
||||
unsubscribe(name: String!, subscriptionId: ID): UnsubscribeResult!
|
||||
updateFilter(input: UpdateFilterInput!): UpdateFilterResult!
|
||||
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
|
||||
updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult!
|
||||
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
|
||||
@ -1629,11 +1632,11 @@ enum SaveFilterErrorCode {
|
||||
}
|
||||
|
||||
input SaveFilterInput {
|
||||
category: String!
|
||||
category: String
|
||||
description: String
|
||||
filter: String!
|
||||
id: ID
|
||||
name: String!
|
||||
position: Int
|
||||
}
|
||||
|
||||
union SaveFilterResult = SaveFilterError | SaveFilterSuccess
|
||||
@ -2174,6 +2177,32 @@ type UnsubscribeSuccess {
|
||||
subscription: Subscription!
|
||||
}
|
||||
|
||||
type UpdateFilterError {
|
||||
errorCodes: [UpdateFilterErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdateFilterErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input UpdateFilterInput {
|
||||
category: String
|
||||
description: String
|
||||
filter: String
|
||||
id: String!
|
||||
name: String
|
||||
position: Int
|
||||
visible: Boolean
|
||||
}
|
||||
|
||||
union UpdateFilterResult = UpdateFilterError | UpdateFilterSuccess
|
||||
|
||||
type UpdateFilterSuccess {
|
||||
filter: Filter!
|
||||
}
|
||||
|
||||
type UpdateHighlightError {
|
||||
errorCodes: [UpdateHighlightErrorCode!]!
|
||||
}
|
||||
|
||||
@ -12,9 +12,13 @@ import {
|
||||
MutationDeleteFilterArgs,
|
||||
MutationMoveFilterArgs,
|
||||
MutationSaveFilterArgs,
|
||||
MutationUpdateFilterArgs,
|
||||
SaveFilterError,
|
||||
SaveFilterErrorCode,
|
||||
SaveFilterSuccess,
|
||||
UpdateFilterError,
|
||||
UpdateFilterSuccess,
|
||||
UpdateFilterErrorCode,
|
||||
} from '../../generated/graphql'
|
||||
import { Filter } from '../../entity/filter'
|
||||
import { getRepository, setClaims } from '../../entity/utils'
|
||||
@ -23,23 +27,24 @@ import { AppDataSource } from '../../server'
|
||||
import { Between } from 'typeorm'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { env } from '../../env'
|
||||
import { isNil, mergeWith } from 'lodash'
|
||||
|
||||
export const saveFilterResolver = authorized<
|
||||
SaveFilterSuccess,
|
||||
SaveFilterError,
|
||||
MutationSaveFilterArgs
|
||||
>(async (_, { input }, { claims, log }) => {
|
||||
>(async (_, { input }, { claims: { uid }, log }) => {
|
||||
log.info('Saving filters', {
|
||||
input,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'saveFilterResolver',
|
||||
uid: claims.uid,
|
||||
uid,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: claims.uid })
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [SaveFilterErrorCode.Unauthorized],
|
||||
@ -47,9 +52,14 @@ export const saveFilterResolver = authorized<
|
||||
}
|
||||
|
||||
const filter = await getRepository(Filter).save({
|
||||
...input,
|
||||
id: input.id ?? undefined,
|
||||
user: { id: claims.uid },
|
||||
user: { id: uid },
|
||||
name: input.name,
|
||||
category: 'Search',
|
||||
description: '',
|
||||
position: input.position ?? 0,
|
||||
filter: input.filter,
|
||||
defaultFilter: false,
|
||||
visible: true,
|
||||
})
|
||||
|
||||
return {
|
||||
@ -61,7 +71,7 @@ export const saveFilterResolver = authorized<
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'saveFilterResolver',
|
||||
uid: claims.uid,
|
||||
uid,
|
||||
},
|
||||
})
|
||||
|
||||
@ -166,6 +176,113 @@ export const filtersResolver = authorized<FiltersSuccess, FiltersError>(
|
||||
}
|
||||
}
|
||||
)
|
||||
const updatePosition = async (
|
||||
uid: string,
|
||||
filter: Filter,
|
||||
newPosition: number
|
||||
) => {
|
||||
const { position } = filter
|
||||
const moveUp = newPosition < position
|
||||
|
||||
// move filter to the new position
|
||||
const updated = await AppDataSource.transaction(async (t) => {
|
||||
await setClaims(t, uid)
|
||||
|
||||
// update the position of the other filters
|
||||
const updated = await t.getRepository(Filter).update(
|
||||
{
|
||||
user: { id: uid },
|
||||
position: Between(
|
||||
Math.min(newPosition, position),
|
||||
Math.max(newPosition, position)
|
||||
),
|
||||
},
|
||||
{
|
||||
position: () => `position + ${moveUp ? 1 : -1}`,
|
||||
}
|
||||
)
|
||||
|
||||
if (!updated.affected) {
|
||||
return null
|
||||
}
|
||||
|
||||
// update the position of the filter
|
||||
return t.getRepository(Filter).save({
|
||||
...filter,
|
||||
position: newPosition,
|
||||
})
|
||||
})
|
||||
|
||||
if (!updated) {
|
||||
throw new Error('unable to update')
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
export const updateFilterResolver = authorized<
|
||||
UpdateFilterSuccess,
|
||||
UpdateFilterError,
|
||||
MutationUpdateFilterArgs
|
||||
>(async (_, { input }, { claims: { uid }, log }) => {
|
||||
const repo = getRepository(Filter)
|
||||
const { id } = input
|
||||
|
||||
try {
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [UpdateFilterErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const filter = await getRepository(Filter).findOne({
|
||||
where: { id, user: { id: uid } },
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!filter) {
|
||||
return {
|
||||
__typename: 'UpdateFilterError',
|
||||
errorCodes: [UpdateFilterErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (filter.user.id !== uid) {
|
||||
return {
|
||||
__typename: 'UpdateFilterError',
|
||||
errorCodes: [UpdateFilterErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNil(input.position) && filter.position != input.position) {
|
||||
await updatePosition(uid, filter, input.position)
|
||||
}
|
||||
|
||||
const updated = await repo.save({
|
||||
...mergeWith({}, filter, input, (a: unknown, b: unknown) =>
|
||||
isNil(b) ? a : undefined
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
__typename: 'UpdateFilterSuccess',
|
||||
filter: updated,
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error Updating filters', {
|
||||
error,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'UpdateFilterResolver',
|
||||
uid,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
__typename: 'UpdateFilterError',
|
||||
errorCodes: [UpdateFilterErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const moveFilterResolver = authorized<
|
||||
MoveFilterSuccess,
|
||||
@ -211,9 +328,8 @@ export const moveFilterResolver = authorized<
|
||||
return { filter }
|
||||
}
|
||||
|
||||
const oldPosition = filter.position
|
||||
// if afterFilterId is not provided, move to the top
|
||||
let newPosition = 1
|
||||
let newPosition = 0
|
||||
if (afterFilterId) {
|
||||
const afterFilter = await getRepository(Filter).findOne({
|
||||
where: { id: afterFilterId },
|
||||
@ -231,35 +347,7 @@ export const moveFilterResolver = authorized<
|
||||
}
|
||||
newPosition = afterFilter.position
|
||||
}
|
||||
const moveUp = newPosition < oldPosition
|
||||
|
||||
// move filter to the new position
|
||||
const updated = await AppDataSource.transaction(async (t) => {
|
||||
await setClaims(t, uid)
|
||||
|
||||
// update the position of the other filters
|
||||
const updated = await t.getRepository(Filter).update(
|
||||
{
|
||||
user: { id: uid },
|
||||
position: Between(
|
||||
Math.min(newPosition, oldPosition),
|
||||
Math.max(newPosition, oldPosition)
|
||||
),
|
||||
},
|
||||
{
|
||||
position: () => `position + ${moveUp ? 1 : -1}`,
|
||||
}
|
||||
)
|
||||
if (!updated.affected) {
|
||||
return null
|
||||
}
|
||||
|
||||
// update the position of the filter
|
||||
return t.getRepository(Filter).save({
|
||||
...filter,
|
||||
position: newPosition,
|
||||
})
|
||||
})
|
||||
const updated = await updatePosition(uid, filter, newPosition)
|
||||
|
||||
if (!updated) {
|
||||
return {
|
||||
|
||||
@ -41,6 +41,7 @@ import {
|
||||
createReminderResolver,
|
||||
deleteAccountResolver,
|
||||
deleteFilterResolver,
|
||||
updateFilterResolver,
|
||||
deleteHighlightResolver,
|
||||
deleteIntegrationResolver,
|
||||
deleteLabelResolver,
|
||||
@ -210,6 +211,7 @@ export const functionResolvers = {
|
||||
importFromIntegration: importFromIntegrationResolver,
|
||||
setFavoriteArticle: setFavoriteArticleResolver,
|
||||
updateSubscription: updateSubscriptionResolver,
|
||||
updateFilter: updateFilterResolver,
|
||||
},
|
||||
Query: {
|
||||
me: getMeUserResolver,
|
||||
@ -391,8 +393,7 @@ export const functionResolvers = {
|
||||
return undefined
|
||||
}
|
||||
const filePath = generateUploadFilePathName(upload.id, upload.fileName)
|
||||
const url = await generateDownloadSignedUrl(filePath)
|
||||
return url
|
||||
return generateDownloadSignedUrl(filePath)
|
||||
}
|
||||
return article.url
|
||||
},
|
||||
|
||||
@ -2165,11 +2165,11 @@ const schema = gql`
|
||||
}
|
||||
|
||||
input SaveFilterInput {
|
||||
id: ID
|
||||
name: String!
|
||||
filter: String!
|
||||
category: String!
|
||||
category: String
|
||||
description: String
|
||||
position: Int
|
||||
}
|
||||
|
||||
union SaveFilterResult = SaveFilterSuccess | SaveFilterError
|
||||
@ -2187,6 +2187,8 @@ const schema = gql`
|
||||
description: String
|
||||
createdAt: Date!
|
||||
updatedAt: Date!
|
||||
defaultFilter: Boolean
|
||||
visible: Boolean
|
||||
}
|
||||
|
||||
type SaveFilterError {
|
||||
@ -2230,6 +2232,32 @@ const schema = gql`
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
input UpdateFilterInput {
|
||||
id: String!
|
||||
name: String
|
||||
filter: String
|
||||
position: Int
|
||||
category: String
|
||||
description: String
|
||||
visible: Boolean
|
||||
}
|
||||
|
||||
enum UpdateFilterErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
union UpdateFilterResult = UpdateFilterSuccess | UpdateFilterError
|
||||
|
||||
type UpdateFilterSuccess {
|
||||
filter: Filter!
|
||||
}
|
||||
|
||||
type UpdateFilterError {
|
||||
errorCodes: [UpdateFilterErrorCode!]!
|
||||
}
|
||||
|
||||
input MoveFilterInput {
|
||||
filterId: ID!
|
||||
afterFilterId: ID # null to move to the top
|
||||
@ -2616,6 +2644,7 @@ const schema = gql`
|
||||
saveFilter(input: SaveFilterInput!): SaveFilterResult!
|
||||
deleteFilter(id: ID!): DeleteFilterResult!
|
||||
moveFilter(input: MoveFilterInput!): MoveFilterResult!
|
||||
updateFilter(input: UpdateFilterInput!): UpdateFilterResult!
|
||||
createGroup(input: CreateGroupInput!): CreateGroupResult!
|
||||
recommend(input: RecommendInput!): RecommendResult!
|
||||
joinGroup(inviteCode: String!): JoinGroupResult!
|
||||
|
||||
@ -11,6 +11,7 @@ import { AppDataSource } from '../server'
|
||||
import { logger } from '../utils/logger'
|
||||
import { validateUsername } from '../utils/usernamePolicy'
|
||||
import { sendConfirmationEmail } from './send_emails'
|
||||
import { Filter } from '../entity/filter'
|
||||
|
||||
export const createUser = async (input: {
|
||||
provider: AuthProvider
|
||||
@ -85,6 +86,9 @@ export const createUser = async (input: {
|
||||
group: invite.group,
|
||||
})
|
||||
}
|
||||
|
||||
await createDefaultFiltersForUser(t)(user.id)
|
||||
|
||||
return [user, profile]
|
||||
}
|
||||
)
|
||||
@ -98,6 +102,29 @@ export const createUser = async (input: {
|
||||
return [user, profile]
|
||||
}
|
||||
|
||||
const createDefaultFiltersForUser =
|
||||
(t: EntityManager) =>
|
||||
async (userId: string): Promise<Filter[]> => {
|
||||
const defaultFilters = [
|
||||
{ name: 'Inbox', filter: 'in:inbox' },
|
||||
{ name: 'Continue Reading', filter: 'in:inbox sort:read-desc is:unread' },
|
||||
{ name: 'Non-Feed Items', filter: 'in:library' },
|
||||
{ name: 'Highlights', filter: 'has:highlights mode:highlights' },
|
||||
{ name: 'Unlabeled', filter: 'no:label' },
|
||||
{ name: 'Oldest First', filter: 'sort:saved-asc' },
|
||||
{ name: 'Files', filter: 'type:file' },
|
||||
{ name: 'Archived', filter: 'in:archive' },
|
||||
].map((it, position) => ({
|
||||
...it,
|
||||
user: { id: userId },
|
||||
position,
|
||||
defaultFilter: true,
|
||||
category: 'Search',
|
||||
}))
|
||||
|
||||
return t.getRepository(Filter).save(defaultFilters)
|
||||
}
|
||||
|
||||
// TODO: Maybe this should be moved into a service
|
||||
const validateInvite = async (
|
||||
entityManager: EntityManager,
|
||||
|
||||
@ -15,6 +15,7 @@ import { getRepository, setClaims } from '../src/entity/utils'
|
||||
import { SubscriptionStatus, SubscriptionType } from '../src/generated/graphql'
|
||||
import { AppDataSource } from '../src/server'
|
||||
import { createUser } from '../src/services/create_user'
|
||||
import { Filter } from "../src/entity/filter"
|
||||
|
||||
const runMigrations = async () => {
|
||||
const migrationDirectory = __dirname + '/../../db/migrations'
|
||||
@ -70,6 +71,19 @@ export const deleteTestUser = async (userId: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteFiltersFromUser = async (userId: string) => {
|
||||
await AppDataSource.transaction(async (t) => {
|
||||
await setClaims(t, userId)
|
||||
const filterRepo = t.getRepository(Filter);
|
||||
|
||||
const userFilters = await filterRepo.findBy({ user: { id: userId }})
|
||||
|
||||
await Promise.all(userFilters.map(filter => {
|
||||
return filterRepo.delete(filter.id)
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
export const createTestUser = async (
|
||||
name: string,
|
||||
invite?: string | undefined,
|
||||
|
||||
@ -2,10 +2,10 @@ import 'mocha'
|
||||
import chai, { expect } from 'chai'
|
||||
import {
|
||||
createTestUser,
|
||||
createUserWithoutProfile,
|
||||
createUserWithoutProfile, deleteFiltersFromUser,
|
||||
deleteTestUser,
|
||||
getProfile,
|
||||
} from '../db'
|
||||
getProfile
|
||||
} from "../db"
|
||||
import { createGroup } from '../../src/services/groups'
|
||||
import {
|
||||
getUserFollowers,
|
||||
@ -18,10 +18,29 @@ import * as util from '../../src/utils/sendEmail'
|
||||
import { MailDataRequired } from '@sendgrid/helpers/classes/mail'
|
||||
import { User } from '../../src/entity/user'
|
||||
import { getRepository } from '../../src/entity/utils'
|
||||
import { Filter } from "../../src/entity/filter"
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
describe('create user', () => {
|
||||
|
||||
context('creates a user through manual sign up', () => {
|
||||
it ('adds the default filters to the user', async () => {
|
||||
after(async () => {
|
||||
const testUser = await getRepository(User).findOneBy({
|
||||
name: 'filter_user',
|
||||
})
|
||||
await deleteTestUser(testUser!.id)
|
||||
await deleteFiltersFromUser(testUser!.id)
|
||||
})
|
||||
|
||||
const user = await createTestUser('filter_user');
|
||||
const filters = await getRepository(Filter).findBy({ user: { id: user.id }})
|
||||
|
||||
expect(filters).not.to.be.empty
|
||||
})
|
||||
})
|
||||
|
||||
context('create a user with an invite', () => {
|
||||
it('follows the other user in the group', async () => {
|
||||
after(async () => {
|
||||
|
||||
69
packages/db/migrations/0119.do.add_defaults_to_filters.sql
Normal file
69
packages/db/migrations/0119.do.add_defaults_to_filters.sql
Normal file
@ -0,0 +1,69 @@
|
||||
-- Type: DO
|
||||
-- Name: add_defaults_to_filters
|
||||
-- Description: Add Defaults to Filters
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.filters
|
||||
ADD COLUMN default_filter boolean NOT NULL DEFAULT false,
|
||||
ADD COLUMN visible boolean NOT NULL DEFAULT true;
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_filter_position()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
new_position INTEGER;
|
||||
BEGIN
|
||||
IF (TG_OP = 'DELETE') THEN
|
||||
UPDATE omnivore.filters SET position = position - 1 WHERE user_id = OLD.user_id AND position > OLD.position;
|
||||
RETURN OLD;
|
||||
ELSIF (TG_OP = 'INSERT' and NEW.position is null) THEN
|
||||
SELECT COALESCE(MAX(position), 0) + 1 INTO new_position FROM omnivore.filters WHERE user_id = NEW.user_id AND name < NEW.name;
|
||||
UPDATE omnivore.filters SET position = position + 1 WHERE user_id = NEW.user_id AND position >= new_position;
|
||||
NEW.position = new_position;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE 'plpgsql';
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search', 'Inbox', 'in:inbox', 0, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search','Continue Reading', 'in:inbox sort:read-desc is:unread', 1, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search', 'Non-Feed Items', 'in:library', 2, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search', 'Highlights', 'has:highlights mode:highlights', 3, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search','Unlabeled', 'no:label', 4, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search','Oldest First', 'sort:saved-asc', 5, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search','Files', 'type:file', 6, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO omnivore.filters (user_id, category, name, filter, position, default_filter)
|
||||
SELECT id, 'Search', 'Archived', 'in:archive', 7, true
|
||||
FROM omnivore.user
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
10
packages/db/migrations/0119.undo.add_defaults_to_filters.sql
Normal file
10
packages/db/migrations/0119.undo.add_defaults_to_filters.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- Type: UNDO
|
||||
-- Name: add_defaults_to_filters
|
||||
-- Description: Add Defaults to Filters
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.filters
|
||||
DROP COLUMN default,
|
||||
DROP COLUMN visible;
|
||||
|
||||
COMMIT;
|
||||
@ -11,7 +11,6 @@ import {
|
||||
DropdownSeparator,
|
||||
} from '../elements/DropdownElements'
|
||||
import GridLayoutIcon from '../elements/images/GridLayoutIcon'
|
||||
import ListLayoutIcon from '../elements/images/ListLayoutIcon'
|
||||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { styled, theme, ThemeId } from '../tokens/stitches.config'
|
||||
@ -37,6 +36,9 @@ export type HeaderDropdownAction =
|
||||
| 'navigate-to-subscriptions'
|
||||
| 'navigate-to-api'
|
||||
| 'navigate-to-integrations'
|
||||
| 'navigate-to-saved-searches'
|
||||
| 'increaseFontSize'
|
||||
| 'decreaseFontSize'
|
||||
| 'logout'
|
||||
|
||||
export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
||||
@ -67,6 +69,9 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
||||
case 'navigate-to-integrations':
|
||||
router.push('/settings/integrations')
|
||||
break
|
||||
case 'navigate-to-saved-searches':
|
||||
router.push('/settings/saved-searches')
|
||||
break;
|
||||
case 'logout':
|
||||
document.dispatchEvent(new Event('logout'))
|
||||
break
|
||||
|
||||
@ -10,6 +10,8 @@ import { theme } from '../../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { LogoBox } from '../../elements/LogoBox'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { useGetSavedSearchQuery} from '../../../lib/networking/queries/useGetSavedSearchQuery'
|
||||
import { SavedSearch } from "../../../lib/networking/fragments/savedSearchFragment"
|
||||
import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
|
||||
import Link from 'next/link'
|
||||
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
|
||||
@ -84,43 +86,16 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
|
||||
}
|
||||
|
||||
function SavedSearches(props: LibraryFilterMenuProps): JSX.Element {
|
||||
const items = [
|
||||
{
|
||||
name: 'Inbox',
|
||||
term: 'in:inbox',
|
||||
},
|
||||
{
|
||||
name: 'Continue Reading',
|
||||
term: 'in:inbox sort:read-desc is:unread',
|
||||
},
|
||||
{
|
||||
name: 'Non-Feed Items',
|
||||
term: 'in:library',
|
||||
},
|
||||
{
|
||||
name: 'Highlights',
|
||||
term: 'has:highlights mode:highlights',
|
||||
},
|
||||
{
|
||||
name: 'Unlabeled',
|
||||
term: 'no:label',
|
||||
},
|
||||
{
|
||||
name: 'Oldest First',
|
||||
term: 'sort:saved-asc',
|
||||
},
|
||||
{
|
||||
name: 'Files',
|
||||
term: 'type:file',
|
||||
},
|
||||
{
|
||||
name: 'Archived',
|
||||
term: 'in:archive',
|
||||
},
|
||||
]
|
||||
const { savedSearches, isLoading } = useGetSavedSearchQuery();
|
||||
|
||||
const sortedSearches = useMemo(() => {
|
||||
return savedSearches?.filter(it => it.visible)?.sort((left: SavedSearch, right: SavedSearch) =>
|
||||
left.position - right.position
|
||||
)
|
||||
}, [savedSearches])
|
||||
|
||||
useRegisterActions(
|
||||
items.map((item, idx) => {
|
||||
(sortedSearches ?? []).map((item, idx) => {
|
||||
const key = String(idx + 1)
|
||||
return {
|
||||
id: `saved_search_${key}`,
|
||||
@ -129,11 +104,11 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element {
|
||||
section: 'Saved Searches',
|
||||
keywords: '?' + item.name,
|
||||
perform: () => {
|
||||
props.applySearchQuery(item.term)
|
||||
props.applySearchQuery(item.filter)
|
||||
},
|
||||
}
|
||||
}),
|
||||
[]
|
||||
[isLoading]
|
||||
)
|
||||
|
||||
const [collapsed, setCollapsed] = usePersistedState<boolean>({
|
||||
@ -142,20 +117,23 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element {
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Saved Searches"
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
<MenuPanel title="Saved Searches"
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
>
|
||||
{!collapsed &&
|
||||
items.map((item) => (
|
||||
<FilterButton
|
||||
key={item.name}
|
||||
text={item.name}
|
||||
filterTerm={item.term}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
{!collapsed && sortedSearches && sortedSearches?.map((item) => (
|
||||
<FilterButton
|
||||
key={item.name}
|
||||
text={item.name}
|
||||
filterTerm={item.filter}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
{!collapsed && (
|
||||
<EditButton
|
||||
title="Edit Saved Searches"
|
||||
destination="/settings/saved-searches"
|
||||
/>)}
|
||||
|
||||
<Box css={{ height: '10px' }}></Box>
|
||||
</MenuPanel>
|
||||
@ -239,6 +217,7 @@ function Labels(props: LibraryFilterMenuProps): JSX.Element {
|
||||
return (
|
||||
<MenuPanel
|
||||
title="Labels"
|
||||
editTitle="Edit Labels"
|
||||
hideBottomBorder={true}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
@ -258,9 +237,9 @@ function Labels(props: LibraryFilterMenuProps): JSX.Element {
|
||||
type MenuPanelProps = {
|
||||
title: string
|
||||
children: ReactNode
|
||||
|
||||
editFunc?: () => void
|
||||
editTitle?: string
|
||||
hideBottomBorder?: boolean
|
||||
|
||||
collapsed: boolean
|
||||
setCollapsed: (collapsed: boolean) => void
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import { FeatureHelpBox } from '../../elements/FeatureHelpBox'
|
||||
|
||||
type SettingsTableProps = {
|
||||
pageId: string
|
||||
pageInfoLink: string
|
||||
pageInfoLink?: string | undefined
|
||||
headerTitle: string
|
||||
|
||||
createTitle?: string
|
||||
@ -375,7 +375,7 @@ export const SettingsTable = (props: SettingsTableProps): JSX.Element => {
|
||||
>
|
||||
{props.headerTitle}
|
||||
</StyledText>
|
||||
<InfoLink href={props.pageInfoLink}></InfoLink>
|
||||
{ props.pageInfoLink && <InfoLink href={props.pageInfoLink}></InfoLink> }
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
21
packages/web/lib/networking/fragments/savedSearchFragment.ts
Normal file
21
packages/web/lib/networking/fragments/savedSearchFragment.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { gql } from "graphql-request"
|
||||
|
||||
export type SavedSearch = {
|
||||
id: string
|
||||
name: string
|
||||
filter: string
|
||||
position: number
|
||||
visible: boolean
|
||||
defaultFilter: boolean
|
||||
}
|
||||
|
||||
export const savedSearchFragment = gql`
|
||||
fragment FiltersFragment on Filter {
|
||||
id
|
||||
name
|
||||
filter
|
||||
position
|
||||
visible
|
||||
defaultFilter
|
||||
}
|
||||
`
|
||||
@ -0,0 +1,36 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch } from "../fragments/savedSearchFragment"
|
||||
|
||||
export type DeleteFilterInput = string
|
||||
|
||||
type DeleteFilterOutput = {
|
||||
deleteFilter: { filter: SavedSearch }
|
||||
}
|
||||
|
||||
export async function deleteFilterMutation (
|
||||
id: DeleteFilterInput
|
||||
): Promise<SavedSearch | undefined> {
|
||||
const mutation = gql`
|
||||
mutation DeleteFilter($id: ID!) {
|
||||
deleteFilter(id: $id) {
|
||||
... on DeleteFilterSuccess {
|
||||
filter {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, { id })
|
||||
const output = data as DeleteFilterOutput | undefined
|
||||
return output?.deleteFilter.filter
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
43
packages/web/lib/networking/mutations/saveFilterMutation.ts
Normal file
43
packages/web/lib/networking/mutations/saveFilterMutation.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch } from "../fragments/savedSearchFragment"
|
||||
|
||||
export type AddFilterInput = {
|
||||
name: string
|
||||
filter: string
|
||||
category: string
|
||||
position: number
|
||||
}
|
||||
|
||||
type AddFilterOutput = {
|
||||
saveFilter: { filter: SavedSearch }
|
||||
}
|
||||
|
||||
export async function saveFilterMutation (
|
||||
input: AddFilterInput
|
||||
): Promise<SavedSearch | undefined> {
|
||||
const mutation = gql`
|
||||
mutation SaveFilter($input: SaveFilterInput!) {
|
||||
saveFilter(input: $input) {
|
||||
... on SaveFilterSuccess {
|
||||
filter {
|
||||
id
|
||||
name
|
||||
filter
|
||||
position
|
||||
visible
|
||||
defaultFilter
|
||||
}
|
||||
}
|
||||
|
||||
... on SaveFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const data = await gqlFetcher(mutation, { input })
|
||||
const output = data as AddFilterOutput | undefined
|
||||
return output?.saveFilter.filter
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch } from "../fragments/savedSearchFragment"
|
||||
|
||||
export type UpdateFilterInput = {
|
||||
id?: string
|
||||
name?: string
|
||||
filter?: string
|
||||
position?: number
|
||||
category?: string
|
||||
description?: string
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
type UpdateFilterOutput = {
|
||||
filter: SavedSearch
|
||||
}
|
||||
|
||||
export async function updateFilterMutation (
|
||||
input: UpdateFilterInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation UpdateFilter($input: UpdateFilterInput!) {
|
||||
updateFilter(input: $input) {
|
||||
... on UpdateFilterSuccess {
|
||||
filter {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
... on UpdateFilterError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const { id, name, visible, filter, position } = input
|
||||
const data = await gqlFetcher(mutation, { input: {id, name, filter, position, visible }})
|
||||
const output = data as UpdateFilterOutput | undefined
|
||||
return output?.filter?.id
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
import { SavedSearch, savedSearchFragment } from "../fragments/savedSearchFragment"
|
||||
|
||||
type SavedSearchResponse = {
|
||||
savedSearches?: SavedSearch[]
|
||||
savedSearchErrors?: unknown
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
type SavedSearchResponseData = {
|
||||
filters: { filters: SavedSearch[] }
|
||||
}
|
||||
|
||||
export function useGetSavedSearchQuery(): SavedSearchResponse {
|
||||
const query = gql`
|
||||
query SavedSearches {
|
||||
filters {
|
||||
... on FiltersSuccess {
|
||||
filters {
|
||||
...FiltersFragment
|
||||
}
|
||||
}
|
||||
... on FiltersError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
|
||||
const { data, error } = useSWR(query, publicGqlFetcher);
|
||||
|
||||
if (data || error) {
|
||||
const { filters } = data as SavedSearchResponseData
|
||||
|
||||
return {
|
||||
savedSearches: filters?.filters ?? [],
|
||||
savedSearchErrors: error ?? {},
|
||||
isLoading: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
savedSearches: [],
|
||||
savedSearchErrors: null,
|
||||
isLoading: true,
|
||||
}
|
||||
}
|
||||
1003
packages/web/pages/settings/saved-searches.tsx
Normal file
1003
packages/web/pages/settings/saved-searches.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user