allow mark as read and add labels bulk actions
This commit is contained in:
@ -18,6 +18,7 @@ import {
|
||||
import { client, INDEX_ALIAS } from './index'
|
||||
import {
|
||||
ArticleSavingRequestStatus,
|
||||
Label,
|
||||
Page,
|
||||
PageContext,
|
||||
PageSearchArgs,
|
||||
@ -424,103 +425,111 @@ export const getPageById = async (id: string): Promise<Page | undefined> => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildSearchBody = (userId: string, args: PageSearchArgs) => {
|
||||
const {
|
||||
query,
|
||||
readFilter = ReadFilter.ALL,
|
||||
typeFilter,
|
||||
labelFilters,
|
||||
inFilter = InFilter.ALL,
|
||||
hasFilters,
|
||||
dateFilters,
|
||||
termFilters,
|
||||
matchFilters,
|
||||
ids,
|
||||
noFilters,
|
||||
siteName,
|
||||
} = args
|
||||
|
||||
const includeLabels = labelFilters?.filter(
|
||||
(filter) => filter.type === LabelFilterType.INCLUDE
|
||||
)
|
||||
const excludeLabels = labelFilters?.filter(
|
||||
(filter) => filter.type === LabelFilterType.EXCLUDE
|
||||
)
|
||||
|
||||
// start building the query
|
||||
let builder = esBuilder().query('term', { userId })
|
||||
|
||||
// append filters
|
||||
if (query) {
|
||||
builder = appendQuery(builder, query)
|
||||
}
|
||||
if (typeFilter) {
|
||||
builder = appendTypeFilter(builder, typeFilter)
|
||||
}
|
||||
if (inFilter !== InFilter.ALL) {
|
||||
builder = appendInFilter(builder, inFilter)
|
||||
}
|
||||
if (readFilter !== ReadFilter.ALL) {
|
||||
builder = appendReadFilter(builder, readFilter)
|
||||
}
|
||||
if (hasFilters && hasFilters.length > 0) {
|
||||
builder = appendHasFilters(builder, hasFilters)
|
||||
}
|
||||
if (includeLabels && includeLabels.length > 0) {
|
||||
builder = appendIncludeLabelFilter(builder, includeLabels)
|
||||
}
|
||||
if (excludeLabels && excludeLabels.length > 0) {
|
||||
builder = appendExcludeLabelFilter(builder, excludeLabels)
|
||||
}
|
||||
if (dateFilters && dateFilters.length > 0) {
|
||||
builder = appendDateFilters(builder, dateFilters)
|
||||
}
|
||||
if (termFilters) {
|
||||
builder = appendTermFilters(builder, termFilters)
|
||||
}
|
||||
if (matchFilters) {
|
||||
builder = appendMatchFilters(builder, matchFilters)
|
||||
}
|
||||
if (ids && ids.length > 0) {
|
||||
builder = appendIdsFilter(builder, ids)
|
||||
}
|
||||
if (args.recommendedBy) {
|
||||
builder = appendRecommendedBy(builder, args.recommendedBy)
|
||||
}
|
||||
if (!args.includePending) {
|
||||
builder = builder.notQuery('term', {
|
||||
state: ArticleSavingRequestStatus.Processing,
|
||||
})
|
||||
}
|
||||
if (!args.includeDeleted) {
|
||||
builder = builder.notQuery('term', {
|
||||
state: ArticleSavingRequestStatus.Deleted,
|
||||
})
|
||||
}
|
||||
if (noFilters) {
|
||||
builder = appendNoFilters(builder, noFilters)
|
||||
}
|
||||
if (siteName) {
|
||||
builder = appendSiteNameFilter(builder, siteName)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
export const searchPages = async (
|
||||
args: PageSearchArgs,
|
||||
userId: string
|
||||
): Promise<[Page[], number] | undefined> => {
|
||||
try {
|
||||
const {
|
||||
from = 0,
|
||||
size = 10,
|
||||
sort,
|
||||
query,
|
||||
readFilter = ReadFilter.ALL,
|
||||
typeFilter,
|
||||
labelFilters,
|
||||
inFilter = InFilter.ALL,
|
||||
hasFilters,
|
||||
dateFilters,
|
||||
termFilters,
|
||||
matchFilters,
|
||||
ids,
|
||||
includeContent,
|
||||
noFilters,
|
||||
siteName,
|
||||
} = args
|
||||
const { from = 0, size = 10, sort, includeContent } = args
|
||||
|
||||
// default order is descending
|
||||
const sortOrder = sort?.order || SortOrder.DESCENDING
|
||||
// default sort by saved_at
|
||||
const sortField = sort?.by || SortBy.SAVED
|
||||
const includeLabels = labelFilters?.filter(
|
||||
(filter) => filter.type === LabelFilterType.INCLUDE
|
||||
)
|
||||
const excludeLabels = labelFilters?.filter(
|
||||
(filter) => filter.type === LabelFilterType.EXCLUDE
|
||||
)
|
||||
// start building the query
|
||||
let builder = esBuilder()
|
||||
.query('term', { userId })
|
||||
|
||||
// build the query
|
||||
const builder = buildSearchBody(userId, args)
|
||||
const body = builder
|
||||
.sort(sortField, sortOrder)
|
||||
.from(from)
|
||||
.size(size)
|
||||
.rawOption('_source', {
|
||||
excludes: includeContent ? [] : ['originalHtml', 'content'],
|
||||
})
|
||||
// append filters
|
||||
if (query) {
|
||||
builder = appendQuery(builder, query)
|
||||
}
|
||||
if (typeFilter) {
|
||||
builder = appendTypeFilter(builder, typeFilter)
|
||||
}
|
||||
if (inFilter !== InFilter.ALL) {
|
||||
builder = appendInFilter(builder, inFilter)
|
||||
}
|
||||
if (readFilter !== ReadFilter.ALL) {
|
||||
builder = appendReadFilter(builder, readFilter)
|
||||
}
|
||||
if (hasFilters && hasFilters.length > 0) {
|
||||
builder = appendHasFilters(builder, hasFilters)
|
||||
}
|
||||
if (includeLabels && includeLabels.length > 0) {
|
||||
builder = appendIncludeLabelFilter(builder, includeLabels)
|
||||
}
|
||||
if (excludeLabels && excludeLabels.length > 0) {
|
||||
builder = appendExcludeLabelFilter(builder, excludeLabels)
|
||||
}
|
||||
if (dateFilters && dateFilters.length > 0) {
|
||||
builder = appendDateFilters(builder, dateFilters)
|
||||
}
|
||||
if (termFilters) {
|
||||
builder = appendTermFilters(builder, termFilters)
|
||||
}
|
||||
if (matchFilters) {
|
||||
builder = appendMatchFilters(builder, matchFilters)
|
||||
}
|
||||
if (ids && ids.length > 0) {
|
||||
builder = appendIdsFilter(builder, ids)
|
||||
}
|
||||
if (args.recommendedBy) {
|
||||
builder = appendRecommendedBy(builder, args.recommendedBy)
|
||||
}
|
||||
if (!args.includePending) {
|
||||
builder = builder.notQuery('term', {
|
||||
state: ArticleSavingRequestStatus.Processing,
|
||||
})
|
||||
}
|
||||
if (!args.includeDeleted) {
|
||||
builder = builder.notQuery('term', {
|
||||
state: ArticleSavingRequestStatus.Deleted,
|
||||
})
|
||||
}
|
||||
if (noFilters) {
|
||||
builder = appendNoFilters(builder, noFilters)
|
||||
}
|
||||
if (siteName) {
|
||||
builder = appendSiteNameFilter(builder, siteName)
|
||||
}
|
||||
// build the query
|
||||
const body = builder.build()
|
||||
.build()
|
||||
|
||||
console.debug('searching pages in elastic', JSON.stringify(body))
|
||||
const response = await client.search<SearchResponse<Page>, BuiltQuery>({
|
||||
@ -688,61 +697,83 @@ export const searchAsYouType = async (
|
||||
|
||||
export const updatePagesAsync = async (
|
||||
userId: string,
|
||||
action: BulkActionType
|
||||
action: BulkActionType,
|
||||
args: PageSearchArgs,
|
||||
labels?: Label[]
|
||||
): Promise<string | null> => {
|
||||
// default action is archive
|
||||
let must_not = [
|
||||
{
|
||||
exists: {
|
||||
field: 'archivedAt',
|
||||
},
|
||||
},
|
||||
]
|
||||
let params: Record<string, unknown> = { archivedAt: new Date() }
|
||||
if (action === BulkActionType.Delete) {
|
||||
must_not = []
|
||||
params = { state: ArticleSavingRequestStatus.Deleted }
|
||||
// build the script
|
||||
let script = {
|
||||
source: '',
|
||||
params: {},
|
||||
}
|
||||
// get update field
|
||||
const field = Object.keys(params)[0]
|
||||
switch (action) {
|
||||
case BulkActionType.Archive:
|
||||
script = {
|
||||
source: `ctx._source.archivedAt = params.archivedAt`,
|
||||
params: {
|
||||
archivedAt: new Date(),
|
||||
},
|
||||
}
|
||||
break
|
||||
case BulkActionType.Delete:
|
||||
script = {
|
||||
source: `ctx._source.state = params.state`,
|
||||
params: {
|
||||
state: ArticleSavingRequestStatus.Deleted,
|
||||
},
|
||||
}
|
||||
break
|
||||
case BulkActionType.AddLabels:
|
||||
script = {
|
||||
source: `ctx._source.labels.addAll(params.labels)`,
|
||||
params: {
|
||||
labels,
|
||||
},
|
||||
}
|
||||
break
|
||||
case BulkActionType.MarkAsRead:
|
||||
script = {
|
||||
source: `ctx._source.readAt = params.readAt;
|
||||
ctx._source.readingProgressPercent = params.readingProgressPercent;
|
||||
ctx._source.readingProgressTopPercent = params.readingProgressTopPercent`,
|
||||
params: {
|
||||
readAt: new Date(),
|
||||
readingProgressPercent: 100,
|
||||
readingProgressTopPercent: 100,
|
||||
},
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('Invalid bulk action')
|
||||
}
|
||||
|
||||
// add updatedAt to the script
|
||||
const updatedScript = {
|
||||
source: `${script.source}; ctx._source.updatedAt = params.updatedAt`,
|
||||
lang: 'painless',
|
||||
params: {
|
||||
...script.params,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
// build the query
|
||||
const searchBody = buildSearchBody(userId, args)
|
||||
.rawOption('script', updatedScript)
|
||||
.build()
|
||||
|
||||
console.debug('updating pages in elastic', JSON.stringify(searchBody))
|
||||
|
||||
try {
|
||||
const { body } = await client.updateByQuery({
|
||||
index: INDEX_ALIAS,
|
||||
conflicts: 'proceed',
|
||||
wait_for_completion: false,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
terms: {
|
||||
state: [
|
||||
ArticleSavingRequestStatus.Succeeded,
|
||||
ArticleSavingRequestStatus.Failed,
|
||||
ArticleSavingRequestStatus.Processing,
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not,
|
||||
},
|
||||
},
|
||||
script: {
|
||||
source: `ctx._source.${field} = params.${field}`,
|
||||
lang: 'painless',
|
||||
params,
|
||||
},
|
||||
},
|
||||
body: searchBody,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (body.failures?.length > 0) {
|
||||
if (body.failures && body.failures.length > 0) {
|
||||
console.log('failed to update pages in elastic', body.failures)
|
||||
return null
|
||||
}
|
||||
|
||||
@ -222,34 +222,16 @@ export type ArticlesSuccess = {
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type BulkAction = {
|
||||
itemIds: Array<Scalars['ID']>;
|
||||
itemType: BulkActionItemType;
|
||||
labelIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
type: BulkActionType;
|
||||
};
|
||||
|
||||
export type BulkActionError = {
|
||||
__typename?: 'BulkActionError';
|
||||
errorCodes: Array<BulkActionErrorCode>;
|
||||
};
|
||||
|
||||
export enum BulkActionErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type BulkActionInput = {
|
||||
actions: Array<BulkAction>;
|
||||
query?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum BulkActionItemType {
|
||||
Highlight = 'HIGHLIGHT',
|
||||
Label = 'LABEL',
|
||||
Page = 'PAGE',
|
||||
Subscription = 'SUBSCRIPTION'
|
||||
}
|
||||
|
||||
export type BulkActionResult = BulkActionError | BulkActionSuccess;
|
||||
|
||||
export type BulkActionSuccess = {
|
||||
@ -1319,7 +1301,9 @@ export type MutationAddPopularReadArgs = {
|
||||
|
||||
|
||||
export type MutationBulkActionArgs = {
|
||||
input: BulkActionInput;
|
||||
action: BulkActionType;
|
||||
labelIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
query: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
@ -3313,11 +3297,8 @@ export type ResolversTypes = {
|
||||
ArticlesResult: ResolversTypes['ArticlesError'] | ResolversTypes['ArticlesSuccess'];
|
||||
ArticlesSuccess: ResolverTypeWrapper<ArticlesSuccess>;
|
||||
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
|
||||
BulkAction: BulkAction;
|
||||
BulkActionError: ResolverTypeWrapper<BulkActionError>;
|
||||
BulkActionErrorCode: BulkActionErrorCode;
|
||||
BulkActionInput: BulkActionInput;
|
||||
BulkActionItemType: BulkActionItemType;
|
||||
BulkActionResult: ResolversTypes['BulkActionError'] | ResolversTypes['BulkActionSuccess'];
|
||||
BulkActionSuccess: ResolverTypeWrapper<BulkActionSuccess>;
|
||||
BulkActionType: BulkActionType;
|
||||
@ -3796,9 +3777,7 @@ export type ResolversParentTypes = {
|
||||
ArticlesResult: ResolversParentTypes['ArticlesError'] | ResolversParentTypes['ArticlesSuccess'];
|
||||
ArticlesSuccess: ArticlesSuccess;
|
||||
Boolean: Scalars['Boolean'];
|
||||
BulkAction: BulkAction;
|
||||
BulkActionError: BulkActionError;
|
||||
BulkActionInput: BulkActionInput;
|
||||
BulkActionResult: ResolversParentTypes['BulkActionError'] | ResolversParentTypes['BulkActionSuccess'];
|
||||
BulkActionSuccess: BulkActionSuccess;
|
||||
CreateArticleError: CreateArticleError;
|
||||
@ -5037,7 +5016,7 @@ export type MoveLabelSuccessResolvers<ContextType = ResolverContext, ParentType
|
||||
|
||||
export type MutationResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = {
|
||||
addPopularRead?: Resolver<ResolversTypes['AddPopularReadResult'], ParentType, ContextType, RequireFields<MutationAddPopularReadArgs, 'name'>>;
|
||||
bulkAction?: Resolver<ResolversTypes['BulkActionResult'], ParentType, ContextType, RequireFields<MutationBulkActionArgs, 'input'>>;
|
||||
bulkAction?: Resolver<ResolversTypes['BulkActionResult'], ParentType, ContextType, RequireFields<MutationBulkActionArgs, 'action' | 'query'>>;
|
||||
createArticle?: Resolver<ResolversTypes['CreateArticleResult'], ParentType, ContextType, RequireFields<MutationCreateArticleArgs, 'input'>>;
|
||||
createArticleSavingRequest?: Resolver<ResolversTypes['CreateArticleSavingRequestResult'], ParentType, ContextType, RequireFields<MutationCreateArticleSavingRequestArgs, 'input'>>;
|
||||
createGroup?: Resolver<ResolversTypes['CreateGroupResult'], ParentType, ContextType, RequireFields<MutationCreateGroupArgs, 'input'>>;
|
||||
|
||||
@ -183,33 +183,15 @@ type ArticlesSuccess {
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
input BulkAction {
|
||||
itemIds: [ID!]!
|
||||
itemType: BulkActionItemType!
|
||||
labelIds: [ID!]
|
||||
type: BulkActionType!
|
||||
}
|
||||
|
||||
type BulkActionError {
|
||||
errorCodes: [BulkActionErrorCode!]!
|
||||
}
|
||||
|
||||
enum BulkActionErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input BulkActionInput {
|
||||
actions: [BulkAction!]!
|
||||
query: String
|
||||
}
|
||||
|
||||
enum BulkActionItemType {
|
||||
HIGHLIGHT
|
||||
LABEL
|
||||
PAGE
|
||||
SUBSCRIPTION
|
||||
}
|
||||
|
||||
union BulkActionResult = BulkActionError | BulkActionSuccess
|
||||
|
||||
type BulkActionSuccess {
|
||||
@ -1111,7 +1093,7 @@ type MoveLabelSuccess {
|
||||
|
||||
type Mutation {
|
||||
addPopularRead(name: String!): AddPopularReadResult!
|
||||
bulkAction(input: BulkActionInput!): BulkActionResult!
|
||||
bulkAction(action: BulkActionType!, labelIds: [ID!], query: String!): BulkActionResult!
|
||||
createArticle(input: CreateArticleInput!): CreateArticleResult!
|
||||
createArticleSavingRequest(input: CreateArticleSavingRequestInput!): CreateArticleSavingRequestResult!
|
||||
createGroup(input: CreateGroupInput!): CreateGroupResult!
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
BulkActionError,
|
||||
BulkActionErrorCode,
|
||||
BulkActionSuccess,
|
||||
BulkActionType,
|
||||
ContentReader,
|
||||
CreateArticleError,
|
||||
CreateArticleErrorCode,
|
||||
@ -73,7 +74,7 @@ import {
|
||||
UpdatesSinceSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { createPageSaveRequest } from '../../services/create_page_save_request'
|
||||
import { createLabels } from '../../services/labels'
|
||||
import { createLabels, getLabelsByIds } from '../../services/labels'
|
||||
import { parsedContentToPage } from '../../services/save_page'
|
||||
import { traceAs } from '../../tracing'
|
||||
import { Merge } from '../../util'
|
||||
@ -1097,26 +1098,43 @@ export const bulkActionResolver = authorized<
|
||||
BulkActionSuccess,
|
||||
BulkActionError,
|
||||
MutationBulkActionArgs
|
||||
>(async (_parent, { action }, { claims: { uid }, log }) => {
|
||||
>(async (_parent, { query, action, labelIds }, { claims: { uid }, log }) => {
|
||||
log.info('bulkActionResolver')
|
||||
|
||||
if (!uid) {
|
||||
log.error('bulkActionResolver', { error: 'Unauthorized' })
|
||||
return { errorCodes: [BulkActionErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: uid,
|
||||
event: 'BulkAction',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
action,
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: get search filters from query
|
||||
if (!uid) {
|
||||
log.log('bulkActionResolver', { error: 'Unauthorized' })
|
||||
return { errorCodes: [BulkActionErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
log.log('bulkActionResolver', { error: 'no query' })
|
||||
return { errorCodes: [BulkActionErrorCode.BadRequest] }
|
||||
}
|
||||
|
||||
// get labels if needed
|
||||
let labels = undefined
|
||||
if (action === BulkActionType.AddLabels) {
|
||||
if (!labelIds || labelIds.length === 0) {
|
||||
return { errorCodes: [BulkActionErrorCode.BadRequest] }
|
||||
}
|
||||
|
||||
labels = await getLabelsByIds(uid, labelIds)
|
||||
}
|
||||
|
||||
// parse query
|
||||
const searchQuery = parseSearchQuery(query)
|
||||
|
||||
// start a task to update pages
|
||||
const taskId = await updatePagesAsync(uid, action)
|
||||
const taskId = await updatePagesAsync(uid, action, searchQuery, labels)
|
||||
|
||||
return { success: !!taskId }
|
||||
})
|
||||
|
||||
@ -2420,25 +2420,6 @@ const schema = gql`
|
||||
ADD_LABELS
|
||||
}
|
||||
|
||||
input BulkAction {
|
||||
itemType: BulkActionItemType!
|
||||
type: BulkActionType!
|
||||
itemIds: [ID!]!
|
||||
labelIds: [ID!]
|
||||
}
|
||||
|
||||
enum BulkActionItemType {
|
||||
PAGE
|
||||
HIGHLIGHT
|
||||
LABEL
|
||||
SUBSCRIPTION
|
||||
}
|
||||
|
||||
input BulkActionInput {
|
||||
query: String
|
||||
actions: [BulkAction!]!
|
||||
}
|
||||
|
||||
union BulkActionResult = BulkActionSuccess | BulkActionError
|
||||
|
||||
type BulkActionSuccess {
|
||||
@ -2451,6 +2432,7 @@ const schema = gql`
|
||||
|
||||
enum BulkActionErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
union ImportFromIntegrationResult =
|
||||
@ -2559,7 +2541,11 @@ const schema = gql`
|
||||
contentType: String!
|
||||
): UploadImportFileResult!
|
||||
markEmailAsItem(recentEmailId: ID!): MarkEmailAsItemResult!
|
||||
bulkAction(input: BulkActionInput!): BulkActionResult!
|
||||
bulkAction(
|
||||
query: String!
|
||||
action: BulkActionType!
|
||||
labelIds: [ID!]
|
||||
): BulkActionResult!
|
||||
importFromIntegration(integrationId: ID!): ImportFromIntegrationResult!
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user