Add Filters, and editing filters.
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 },
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!filter) {
|
||||
return {
|
||||
__typename: 'UpdateFilterError',
|
||||
errorCodes: [UpdateFilterErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
if (filter.user.id !== uid) {
|
||||
return {
|
||||
__typename: 'UpdateFilterError',
|
||||
errorCodes: [UpdateFilterErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
if (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,
|
||||
|
||||
@ -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,27 @@ 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: 'Read Later', filter: 'in:library' },
|
||||
{ name: 'Highlights', filter: 'has:highlights mode:highlights' },
|
||||
{ name: 'Unlabelled', filter: 'no:label' },
|
||||
{ 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,
|
||||
|
||||
59
packages/db/migrations/0119.do.add_defaults_to_filters.sql
Normal file
59
packages/db/migrations/0119.do.add_defaults_to_filters.sql
Normal file
@ -0,0 +1,59 @@
|
||||
-- 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', 'Read Later', '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','Unlabelled', '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', 'Archived', 'in:archive', 5, 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,7 @@ export type HeaderDropdownAction =
|
||||
| 'navigate-to-subscriptions'
|
||||
| 'navigate-to-api'
|
||||
| 'navigate-to-integrations'
|
||||
| 'navigate-to-saved-searches'
|
||||
| 'increaseFontSize'
|
||||
| 'decreaseFontSize'
|
||||
| 'logout'
|
||||
@ -66,6 +66,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
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { CaretRight, Circle, DotsThree, Plus } from 'phosphor-react'
|
||||
import { CaretRight, Circle, DotsThree } from 'phosphor-react'
|
||||
import { useGetSubscriptionsQuery } from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
@ -11,6 +11,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"
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '233px'
|
||||
|
||||
@ -82,39 +84,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: 'Read Later',
|
||||
term: 'in:library',
|
||||
},
|
||||
{
|
||||
name: 'Highlights',
|
||||
term: 'has:highlights mode:highlights',
|
||||
},
|
||||
{
|
||||
name: 'Unlabeled',
|
||||
term: 'no:label',
|
||||
},
|
||||
{
|
||||
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}`,
|
||||
@ -123,20 +102,22 @@ function SavedSearches(props: LibraryFilterMenuProps): JSX.Element {
|
||||
section: 'Saved Searches',
|
||||
keywords: '?' + item.name,
|
||||
perform: () => {
|
||||
props.applySearchQuery(item.term)
|
||||
props.applySearchQuery(item.filter)
|
||||
},
|
||||
}
|
||||
}),
|
||||
[]
|
||||
[isLoading]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuPanel title="Saved Searches">
|
||||
{items.map((item) => (
|
||||
<MenuPanel title="Saved Searches" editTitle="Edit Saved Searches" editFunc={() => {
|
||||
window.location.href = '/settings/saved-searches'
|
||||
}}>
|
||||
{sortedSearches && sortedSearches?.map((item) => (
|
||||
<FilterButton
|
||||
key={item.name}
|
||||
text={item.name}
|
||||
filterTerm={item.term}
|
||||
filterTerm={item.filter}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -11,7 +11,7 @@ import { SettingsLayout } from '../SettingsLayout'
|
||||
|
||||
type SettingsTableProps = {
|
||||
pageId: string
|
||||
pageInfoLink: string
|
||||
pageInfoLink?: string | undefined
|
||||
headerTitle: string
|
||||
|
||||
createTitle?: string
|
||||
@ -342,7 +342,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,
|
||||
}
|
||||
}
|
||||
998
packages/web/pages/settings/saved-searches.tsx
Normal file
998
packages/web/pages/settings/saved-searches.tsx
Normal file
@ -0,0 +1,998 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react"
|
||||
import { SettingsLayout } from '../../components/templates/SettingsLayout'
|
||||
import { Button } from '../../components/elements/Button'
|
||||
import { styled } from '../../components/tokens/stitches.config'
|
||||
import {
|
||||
Box,
|
||||
SpanBox,
|
||||
HStack,
|
||||
VStack,
|
||||
} from '../../components/elements/LayoutPrimitives'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { applyStoredTheme, isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import {
|
||||
DotsThree,
|
||||
PencilSimple,
|
||||
Trash,
|
||||
Plus,
|
||||
ArrowsDownUp,
|
||||
} from 'phosphor-react'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownOption,
|
||||
} from '../../components/elements/DropdownElements'
|
||||
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
||||
import { InfoLink } from '../../components/elements/InfoLink'
|
||||
import { useGetSavedSearchQuery } from "../../lib/networking/queries/useGetSavedSearchQuery"
|
||||
import { SavedSearch } from "../../lib/networking/fragments/savedSearchFragment"
|
||||
import CheckboxComponent from "../../components/elements/Checkbox"
|
||||
import { updateFilterMutation } from "../../lib/networking/mutations/updateFilterMutation"
|
||||
import { saveFilterMutation } from "../../lib/networking/mutations/saveFilterMutation"
|
||||
import { inRange } from 'lodash';
|
||||
import { deleteFilterMutation } from "../../lib/networking/mutations/deleteFilterMutation"
|
||||
|
||||
const HeaderWrapper = styled(Box, {
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const TableCard = styled(Box, {
|
||||
padding: '0px',
|
||||
backgroundColor: '$grayBg',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '0.3px solid $grayBgActive',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
paddingLeft: '0',
|
||||
},
|
||||
})
|
||||
|
||||
const TableCardBox = styled(Box, {
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridGap: '$1',
|
||||
gridTemplateColumns: '3fr 1fr',
|
||||
'.showHidden': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'.showHidden': {
|
||||
display: 'unset',
|
||||
gridColumn: 'span 2',
|
||||
width: '100%',
|
||||
padding: '$2 $3 0 $3',
|
||||
},
|
||||
},
|
||||
'@md': {
|
||||
gridTemplateColumns: '20% 15% 1fr 1fr 1fr',
|
||||
'&:hover': {
|
||||
'.showHidden': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const inputStyles = {
|
||||
height: '35px',
|
||||
backgroundColor: 'transparent',
|
||||
color: '$grayTextContrast',
|
||||
padding: '6px 6px',
|
||||
margin: '$2 0',
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '6px',
|
||||
fontSize: '16px',
|
||||
FontFamily: '$fontFamily',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
width: 'auto',
|
||||
minWidth: '180px',
|
||||
},
|
||||
'&[disabled]': {
|
||||
border: 'none',
|
||||
},
|
||||
'&:focus': {
|
||||
outlineColor: '$omnivoreYellow',
|
||||
outlineStyle: 'solid',
|
||||
},
|
||||
}
|
||||
|
||||
const ActionsWrapper = styled(Box, {
|
||||
mr: '$1',
|
||||
display: 'flex',
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
bg: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inter',
|
||||
fontSize: '$2',
|
||||
lineHeight: '1.25',
|
||||
color: '$grayText',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
})
|
||||
|
||||
const IconButton = styled(Button, {
|
||||
variants: {
|
||||
style: {
|
||||
ctaWhite: {
|
||||
color: 'red',
|
||||
padding: '10px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
border: '1px solid $grayBorder',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: 6,
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const TOP_SETTINGS_PANEL = 147
|
||||
const HEIGHT_SETTING_CARD = 56
|
||||
|
||||
const Input = styled('input', { ...inputStyles })
|
||||
|
||||
const TextArea = styled('textarea', { ...inputStyles })
|
||||
|
||||
export default function SavedSearchesPage(): JSX.Element {
|
||||
const { savedSearches, isLoading } = useGetSavedSearchQuery()
|
||||
const [nameInputText, setNameInputText] = useState<string>('')
|
||||
const [queryInputText, setQueryInputText] = useState<string>('')
|
||||
const [editingId, setEditingId] = useState<string|null>(null);
|
||||
const [isCreateMode, setIsCreateMode] = useState<boolean>(false)
|
||||
const [windowWidth, setWindowWidth] = useState<number>(0)
|
||||
const [confirmRemoveSavedSearchId, setConfirmRemoveSavedSearchId] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const [draggedElementId, setDraggedElementId] = useState<string | null>(null);
|
||||
const [draggedElementPosition, setDraggedElementPosition] = useState<{ x: number, y: number } | null>(null);
|
||||
const [sortedSavedSearch, setSortedSavedSearch] = useState<SavedSearch[]>([]);
|
||||
|
||||
// Some theming stuff here.
|
||||
const breakpoint = 768
|
||||
applyStoredTheme(false)
|
||||
|
||||
useEffect(() => {
|
||||
setSortedSavedSearch([...(savedSearches ?? [])].sort((l, r) => l.position - r.position))
|
||||
}, [isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
const handleResizeWindow = () => setWindowWidth(window.innerWidth)
|
||||
if (windowWidth === 0) {
|
||||
setWindowWidth(window.innerWidth)
|
||||
}
|
||||
window.addEventListener('resize', handleResizeWindow)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResizeWindow)
|
||||
}
|
||||
}, [windowWidth])
|
||||
|
||||
const resetSavedSearchState = () => {
|
||||
setIsCreateMode(false)
|
||||
setNameInputText('')
|
||||
setQueryInputText('')
|
||||
setEditingId(null)
|
||||
}
|
||||
|
||||
async function createSavedSearch(): Promise<void> {
|
||||
try {
|
||||
const savedFilter = await saveFilterMutation({ name: nameInputText, filter: queryInputText, category: 'Search', position: sortedSavedSearch?.length ?? 0 });
|
||||
showSuccessToast(`Added Filter: ${nameInputText}`)
|
||||
|
||||
if (savedFilter) {
|
||||
setSortedSavedSearch([...sortedSavedSearch, savedFilter])
|
||||
}
|
||||
resetSavedSearchState()
|
||||
} catch (e) {
|
||||
showErrorToast('Unable to create filter. Unknown error occurred.')
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSavedSearch(id: string): Promise<void> {
|
||||
resetSavedSearchState()
|
||||
|
||||
const changedSortedSearch = sortedSavedSearch?.find(it => it.id == id)
|
||||
if (changedSortedSearch != undefined) {
|
||||
changedSortedSearch.name = nameInputText
|
||||
changedSortedSearch.filter = queryInputText
|
||||
setSortedSavedSearch(sortedSavedSearch)
|
||||
await updateFilterMutation( { id, name: nameInputText, filter: queryInputText });
|
||||
}
|
||||
}
|
||||
|
||||
const onEditPress = (savedSearch: SavedSearch | null) => {
|
||||
if (savedSearch) {
|
||||
setNameInputText(savedSearch.name)
|
||||
setQueryInputText(savedSearch.filter || '')
|
||||
setEditingId(savedSearch.id)
|
||||
} else {
|
||||
resetSavedSearchState()
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteSavedSearch(id: string): Promise<void> {
|
||||
await deleteFilterMutation(id)
|
||||
const currentElement = sortedSavedSearch?.find((it) => it.id == id);
|
||||
|
||||
setSortedSavedSearch(
|
||||
sortedSavedSearch
|
||||
.filter(it => it.id !== id)
|
||||
.map(it => {
|
||||
return { ...it, position: currentElement?.position > it.position ? it.position : it.position - 1}
|
||||
})
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
async function deleteSavedSearch(id: string): Promise<void> {
|
||||
setConfirmRemoveSavedSearchId(id)
|
||||
}
|
||||
|
||||
async function updatePositionOnMouseUp(y: number): Promise<string | undefined> {
|
||||
const idx = Math.floor(((y - 25) - TOP_SETTINGS_PANEL) / HEIGHT_SETTING_CARD)
|
||||
const correctedIdx = Math.min(Math.max(idx, 0), sortedSavedSearch?.length - 1)
|
||||
const currentElement = sortedSavedSearch?.find(({ id }) => id == draggedElementId);
|
||||
const moveUp = correctedIdx < currentElement?.position
|
||||
|
||||
if (correctedIdx != currentElement?.position) {
|
||||
const newlyOrdered = sortedSavedSearch
|
||||
?.map((search) => {
|
||||
let pos = search.position;
|
||||
if (inRange(pos, Math.min(correctedIdx, currentElement.position), Math.max(correctedIdx, currentElement.position)) || search.position == correctedIdx) {
|
||||
pos = search.position + (moveUp ? +1 : -1)
|
||||
console.log(pos)
|
||||
}
|
||||
if (draggedElementId == search?.id) {
|
||||
pos = correctedIdx
|
||||
}
|
||||
return {
|
||||
...search,
|
||||
position: pos
|
||||
}
|
||||
})
|
||||
?.sort((l, r) => l.position - r.position);
|
||||
setSortedSavedSearch(newlyOrdered)
|
||||
console.log(newlyOrdered)
|
||||
return updateFilterMutation({ ...currentElement, position: correctedIdx })
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
<HStack css={{ width: '100%', height: '100%' }}>
|
||||
<VStack
|
||||
css={{
|
||||
mx: '10px',
|
||||
color: '$grayText',
|
||||
width: '100%',
|
||||
maxWidth: '865px',
|
||||
}}
|
||||
>
|
||||
{confirmRemoveSavedSearchId ? (
|
||||
<ConfirmationModal
|
||||
message={
|
||||
'Are you sure?'
|
||||
}
|
||||
onAccept={() => {
|
||||
onDeleteSavedSearch(confirmRemoveSavedSearchId)
|
||||
setConfirmRemoveSavedSearchId(null)
|
||||
}}
|
||||
onOpenChange={() => setConfirmRemoveSavedSearchId(null)}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderWrapper>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<StyledText style="fixedHeadline">Saved Searches </StyledText>
|
||||
</Box>
|
||||
<InfoLink href="/help/search" />
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
{isCreateMode ? null : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetSavedSearchState()
|
||||
setIsCreateMode(true)
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SpanBox>Add Saved Search</SpanBox>
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
p: '0',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</SpanBox>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</HeaderWrapper>
|
||||
<>
|
||||
{isCreateMode ? (
|
||||
windowWidth > breakpoint ? (
|
||||
<DesktopEditCard
|
||||
savedSearch={null}
|
||||
editingId={editingId}
|
||||
setEditingId={setEditingId}
|
||||
isCreateMode={isCreateMode}
|
||||
deleteSavedSearch={deleteSavedSearch}
|
||||
nameInputText={nameInputText}
|
||||
queryInputText={queryInputText}
|
||||
setNameInputText={setNameInputText}
|
||||
setQueryInputText={setQueryInputText}
|
||||
setIsCreateMode={setIsCreateMode}
|
||||
createSavedSearch={createSavedSearch}
|
||||
updateSavedSearch={updateSavedSearch}
|
||||
onEditPress={onEditPress}
|
||||
resetState={resetSavedSearchState}
|
||||
draggedElementId={draggedElementId}
|
||||
setDraggedElementId={setDraggedElementId}
|
||||
setDraggedElementPosition={setDraggedElementPosition}
|
||||
/>
|
||||
) : (
|
||||
<MobileEditCard
|
||||
savedSearch={null}
|
||||
editingId={editingId}
|
||||
setEditingId={setEditingId}
|
||||
isCreateMode={isCreateMode}
|
||||
deleteSavedSearch={deleteSavedSearch}
|
||||
nameInputText={nameInputText}
|
||||
queryInputText={queryInputText}
|
||||
setNameInputText={setNameInputText}
|
||||
setQueryInputText={setQueryInputText}
|
||||
setIsCreateMode={setIsCreateMode}
|
||||
createSavedSearch={createSavedSearch}
|
||||
updateSavedSearch={updateSavedSearch}
|
||||
onEditPress={onEditPress}
|
||||
resetState={resetSavedSearchState}
|
||||
draggedElementId={draggedElementId}
|
||||
setDraggedElementId={setDraggedElementId}
|
||||
setDraggedElementPosition={setDraggedElementPosition}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</>
|
||||
{sortedSavedSearch
|
||||
? sortedSavedSearch.map((savedSearch, i) => {
|
||||
const isLastChild = i === sortedSavedSearch.length - 1
|
||||
const isFirstChild = i === 0
|
||||
const positionY = TOP_SETTINGS_PANEL + (HEIGHT_SETTING_CARD * (i + 1));
|
||||
const cardProps = {
|
||||
savedSearch,
|
||||
editingId,
|
||||
isCreateMode: isCreateMode,
|
||||
isLastChild: isLastChild,
|
||||
isFirstChild: isFirstChild,
|
||||
setEditingId,
|
||||
nameInputText: nameInputText,
|
||||
queryInputText: queryInputText,
|
||||
setNameInputText: setNameInputText,
|
||||
setQueryInputText: setQueryInputText,
|
||||
setIsCreateMode: setIsCreateMode,
|
||||
resetState: resetSavedSearchState,
|
||||
updateSavedSearch,
|
||||
deleteSavedSearch,
|
||||
createSavedSearch,
|
||||
draggedElementId,
|
||||
setDraggedElementId,
|
||||
onEditPress,
|
||||
setDraggedElementPosition,
|
||||
isSwappedCard: draggedElementId != savedSearch.id && (draggedElementPosition?.y + HEIGHT_SETTING_CARD)> positionY && draggedElementPosition?.y + HEIGHT_SETTING_CARD < positionY + HEIGHT_SETTING_CARD,
|
||||
updatePositionOnMouseUp
|
||||
}
|
||||
if (editingId == savedSearch.id) {
|
||||
if (windowWidth >= breakpoint) {
|
||||
return <DesktopEditCard {...cardProps} />
|
||||
} else {
|
||||
return <MobileEditCard {...cardProps} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericTableCard
|
||||
key={savedSearch.id}
|
||||
{...cardProps}
|
||||
onEditPress={onEditPress}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</VStack>
|
||||
</HStack>
|
||||
<Box css={{ height: '120px' }} />
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
type EditCardProps = {
|
||||
savedSearch: SavedSearch | null
|
||||
editingId: string | null
|
||||
nameInputText: string
|
||||
queryInputText: string
|
||||
setQueryInputText: Dispatch<SetStateAction<string>>
|
||||
isCreateMode: boolean
|
||||
setIsCreateMode: Dispatch<SetStateAction<boolean>>
|
||||
setEditingId: Dispatch<SetStateAction<string | null>>
|
||||
setNameInputText: Dispatch<SetStateAction<string>>
|
||||
createSavedSearch: () => Promise<void>
|
||||
updateSavedSearch: (id: string) => Promise<void>
|
||||
deleteSavedSearch: (id: string) => Promise<void>
|
||||
resetState: () => void
|
||||
onEditPress: (savedSearch: SavedSearch | null) => void
|
||||
isFirstChild?: boolean | undefined
|
||||
isLastChild?: boolean | undefined
|
||||
draggedElementId: string | null
|
||||
setDraggedElementId: Dispatch<SetStateAction<string | null>>
|
||||
setDraggedElementPosition: Dispatch<SetStateAction<{ x: number, y: number } | null>>
|
||||
isSwappedCard: boolean,
|
||||
updatePositionOnMouseUp: (y: number) => Promise<void>
|
||||
}
|
||||
|
||||
function GenericTableCard(
|
||||
props: EditCardProps & {
|
||||
isLastChild?: boolean
|
||||
isFirstChild?: boolean
|
||||
}
|
||||
) {
|
||||
const {
|
||||
savedSearch,
|
||||
isLastChild,
|
||||
isFirstChild,
|
||||
editingId,
|
||||
isCreateMode,
|
||||
nameInputText,
|
||||
queryInputText,
|
||||
setQueryInputText,
|
||||
setEditingId,
|
||||
deleteSavedSearch,
|
||||
setNameInputText,
|
||||
createSavedSearch,
|
||||
updateSavedSearch,
|
||||
onEditPress,
|
||||
resetState,
|
||||
draggedElementId,
|
||||
setDraggedElementId,
|
||||
setDraggedElementPosition,
|
||||
isSwappedCard,
|
||||
updatePositionOnMouseUp
|
||||
} = props
|
||||
const [isVisible, setIsVisible] = useState(!!savedSearch?.visible)
|
||||
const showInput = editingId === savedSearch?.id || (isCreateMode && !savedSearch)
|
||||
const iconColor = isDarkTheme() ? '#D8D7D5' : '#5F5E58'
|
||||
const DEFAULT_STYLE = { position: null };
|
||||
const [style, setStyle] = useState<Partial<{ position: string | null, top: string, left: string }>>(DEFAULT_STYLE)
|
||||
const handleEdit = () => {
|
||||
editingId && updateSavedSearch(editingId)
|
||||
setEditingId(null)
|
||||
}
|
||||
const moreActionsButton = () => {
|
||||
return (
|
||||
<ActionsWrapper>
|
||||
<Dropdown
|
||||
disabled={isCreateMode}
|
||||
triggerElement={<DotsThree size={24} color={iconColor} />}
|
||||
>
|
||||
<DropdownOption onSelect={() => null}>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
mr: '0px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
}}
|
||||
onClick={() => onEditPress(savedSearch)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<PencilSimple size={24} color={"black"} />
|
||||
<StyledText
|
||||
color="$grayText"
|
||||
css={{ m: '0px', fontSize: '$5', marginLeft: '$2' }}
|
||||
>
|
||||
Edit
|
||||
</StyledText>
|
||||
</Button>
|
||||
</DropdownOption>
|
||||
<DropdownOption onSelect={() => null}>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
mr: '$1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
}}
|
||||
onClick={() => (savedSearch ? deleteSavedSearch(savedSearch.id) : null)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<Trash size={24} color="#AA2D11" />
|
||||
<StyledText
|
||||
css={{
|
||||
m: '0px',
|
||||
fontSize: '$5',
|
||||
marginLeft: '$2',
|
||||
color: '#AA2D11',
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</StyledText>
|
||||
</Button>
|
||||
</DropdownOption>
|
||||
</Dropdown>
|
||||
</ActionsWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const onMouseDown= (e: MouseEvent) => {
|
||||
if (savedSearch) {
|
||||
setDraggedElementId(savedSearch.id)
|
||||
setDraggedElementPosition({ y: e.clientY - 25, x: e.clientX - 25})
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = async (e: MouseEvent) => {
|
||||
const updatePosition = updatePositionOnMouseUp(e.clientY)
|
||||
setDraggedElementId(null);
|
||||
setStyle(DEFAULT_STYLE);
|
||||
setDraggedElementPosition(null);
|
||||
await updatePosition;
|
||||
}
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (draggedElementId != null && draggedElementId == savedSearch?.id) {
|
||||
setStyle({ position: "absolute", top: `${e.clientY - 25}px`, left: `${e.clientX - 25}px`, maxWidth: '865px' });
|
||||
setDraggedElementPosition({ y: e.clientY - 25, x: e.clientX - 25});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
}, [draggedElementId])
|
||||
|
||||
const setVisibility = () => {
|
||||
updateFilterMutation({ ...savedSearch, visible: !isVisible} )
|
||||
setIsVisible(!isVisible);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
className={"tableCard"}
|
||||
css={{
|
||||
...style,
|
||||
background: 'white',
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 234, 159, 0.12)',
|
||||
},
|
||||
borderTop: isSwappedCard ? "56px solid white" : undefined,
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}
|
||||
>
|
||||
|
||||
<TableCardBox
|
||||
css={{
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridGap: '$1',
|
||||
gridTemplateColumns: '1fr 2fr',
|
||||
height: editingId == savedSearch?.id ? '120px' : '56px',
|
||||
'.showHidden': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'.showHidden': {
|
||||
display: 'unset',
|
||||
gridColumn: 'span 2',
|
||||
width: '100%',
|
||||
padding: '$2 $3 0 $3',
|
||||
},
|
||||
},
|
||||
'@md': {
|
||||
height: '56px',
|
||||
gridTemplateColumns: '4% 3% 20% 28% 1fr 1fr',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0 5px',
|
||||
}}
|
||||
>
|
||||
<ArrowsDownUp size={28} style={{ cursor: 'grab' }} onMouseDown={onMouseDown} onMouseUp={onMouseUp} />
|
||||
</HStack>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0 5px',
|
||||
}}
|
||||
>
|
||||
<CheckboxComponent checked={isVisible} setChecked={setVisibility} />
|
||||
</HStack>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0 5px',
|
||||
}}
|
||||
>
|
||||
{showInput && !savedSearch ? (
|
||||
<SpanBox
|
||||
css={{
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</SpanBox>
|
||||
) : (
|
||||
<StyledText
|
||||
style="body"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
fontSize: '14px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
paddingLeft: '15px'
|
||||
}}
|
||||
>
|
||||
{editingId === savedSearch?.id
|
||||
? nameInputText
|
||||
: savedSearch?.name || ''}
|
||||
</StyledText>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showInput ? (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Query (e.g. in:inbox)"
|
||||
value={queryInputText}
|
||||
onChange={(event) => setQueryInputText(event.target.value)}
|
||||
autoFocus={!!savedSearch}
|
||||
/>
|
||||
) : (
|
||||
<StyledText
|
||||
style="body"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
fontSize: '14px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{editingId === savedSearch?.id
|
||||
? queryInputText
|
||||
: savedSearch?.filter || ''}
|
||||
</StyledText>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
css={{
|
||||
padding: '4px 8px',
|
||||
paddingLeft: '10px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{!showInput && (
|
||||
<Box css={{ marginLeft: 'auto', '@md': { display: 'none' } }}>
|
||||
{moreActionsButton()}
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
ml: '8px',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showInput && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="The search query to execute (e.g. in:inbox)"
|
||||
value={queryInputText}
|
||||
onChange={(event) => setQueryInputText(event.target.value)}
|
||||
autoFocus={!!savedSearch}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0px 8px',
|
||||
}}
|
||||
>
|
||||
{(editingId === savedSearch?.id || !savedSearch) ? (
|
||||
<>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ my: '0px', mr: '$1' }}
|
||||
onClick={() => (savedSearch ? handleEdit() : createSavedSearch())}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="end"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{ mr: '$1', background: '$labelButtonsBg', display: savedSearch?.defaultFilter ? "none" : "block" }}
|
||||
onClick={() => onEditPress(savedSearch)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<PencilSimple size={16} color={iconColor} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{ mr: '$1', background: '$labelButtonsBg', display: savedSearch?.defaultFilter ? "none" : "block" }}
|
||||
onClick={() => deleteSavedSearch(savedSearch.id)}
|
||||
disabled={isCreateMode || savedSearch?.defaultFilter}
|
||||
>
|
||||
<Trash size={16} color={iconColor} />
|
||||
</IconButton>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</TableCardBox>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
function MobileEditCard(props: EditCardProps) {
|
||||
const {
|
||||
savedSearch,
|
||||
editingId,
|
||||
setEditingId,
|
||||
isCreateMode,
|
||||
nameInputText,
|
||||
setNameInputText,
|
||||
queryInputText,
|
||||
setQueryInputText,
|
||||
createSavedSearch,
|
||||
resetState,
|
||||
updateSavedSearch,
|
||||
isFirstChild,
|
||||
isLastChild,
|
||||
} = props
|
||||
|
||||
const handleEdit = () => {
|
||||
editingId && setEditingId(editingId)
|
||||
setEditingId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
css={{
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}
|
||||
>
|
||||
<VStack distribution="center" css={{ width: '100%', margin: '8px' }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
placeholder="Query (e.g. in:inbox)"
|
||||
value={queryInputText}
|
||||
onChange={(event) => setQueryInputText(event.target.value)}
|
||||
rows={5}
|
||||
/>
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{ width: '100%', margin: '$1 0' }}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => (savedSearch ? handleEdit() : createSavedSearch())}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopEditCard(props: EditCardProps) {
|
||||
const {
|
||||
savedSearch,
|
||||
editingId,
|
||||
setEditingId,
|
||||
nameInputText,
|
||||
setNameInputText,
|
||||
queryInputText,
|
||||
setQueryInputText,
|
||||
createSavedSearch,
|
||||
resetState,
|
||||
updateSavedSearch,
|
||||
isFirstChild,
|
||||
isLastChild,
|
||||
} = props
|
||||
|
||||
const handleEdit = () => {
|
||||
editingId && updateSavedSearch(editingId)
|
||||
setEditingId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
css={{
|
||||
width: '100%',
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
distribution="center"
|
||||
css={{ width: '100%', my: '8px', ml: '8px', mr: '0px' }}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ pt: '6px', px: '13px', width: '100%', gap: '16px' }}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Query (e.g. in:inbox)"
|
||||
value={queryInputText}
|
||||
onChange={(event) => setQueryInputText(event.target.value)}
|
||||
/>
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{ marginLeft: 'auto', width: '100% ' }}
|
||||
>
|
||||
<Button
|
||||
style="ctaOutlineYellow"
|
||||
css={{ mr: '12px' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{}}
|
||||
onClick={() => (savedSearch ? handleEdit() : createSavedSearch())}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user