Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
bdf620f
feat: add mark as bot, and reload page after
ulemons Jan 28, 2026
483ffef
fix: caching issue
ulemons Jan 28, 2026
ea50c95
fix: refetch
ulemons Jan 28, 2026
2a0fe55
fix: refetch
ulemons Jan 28, 2026
2fcc932
fix: adding logs
ulemons Jan 28, 2026
bb6e28a
fix: optiomize cache
ulemons Jan 28, 2026
ba03643
fix: optiomize cache
ulemons Jan 28, 2026
70fe0cd
fix: adding logs
ulemons Jan 28, 2026
3fd48b0
fix: adding logs
ulemons Jan 28, 2026
bfa2ba5
fix: simplify code
ulemons Jan 28, 2026
424e274
fix: simplify code
ulemons Jan 28, 2026
d1dd91c
fix: simplify cache handling
ulemons Jan 28, 2026
47531ca
fix: avoid data loss
ulemons Jan 28, 2026
7a3d524
fix: simplify cache, add default params
ulemons Jan 28, 2026
bdefed0
fix: lint
ulemons Jan 28, 2026
6905115
fix: adding logs
ulemons Jan 28, 2026
5c090f1
fix: inlcudeAll propagation
ulemons Jan 28, 2026
1739e2e
fix: inlcudeAll propagation
ulemons Jan 28, 2026
6d71817
fix: inlcudeAll propagation
ulemons Jan 28, 2026
c683776
fix: lint
ulemons Jan 29, 2026
955810a
fix: lint
ulemons Jan 29, 2026
b7d9a11
fix: optimize cache
ulemons Jan 29, 2026
857cdba
fix: test with not fetch
ulemons Jan 29, 2026
225abd8
fix: test with not fetch
ulemons Jan 29, 2026
a590c9b
fix: test with not fetch
ulemons Jan 29, 2026
e45e9f4
fix: invalidation for members
ulemons Jan 29, 2026
1098ff8
fix: invalidation for members
ulemons Jan 29, 2026
4b22166
fix: invalidation for members
ulemons Jan 29, 2026
8e809db
fix: invalidation for members with fetch
ulemons Jan 29, 2026
dbd02e6
fix: test only queryClient
ulemons Jan 29, 2026
ba30b19
fix: add refetch
ulemons Jan 29, 2026
28b86c4
fix: add refetch
ulemons Jan 29, 2026
c77ec5b
fix: add refetch
ulemons Jan 29, 2026
84c6740
fix: add refetch
ulemons Jan 29, 2026
c9d9bfb
fix: revert to TanStack
ulemons Jan 29, 2026
3859bfd
fix: clean up
ulemons Jan 29, 2026
965d800
fix: refetch for members
ulemons Jan 29, 2026
5857cab
fix: refetch for members
ulemons Jan 29, 2026
c54e5f8
fix: lint
ulemons Jan 29, 2026
f046221
fix: adjust timers
ulemons Jan 29, 2026
cc574d3
fix: remove refetch
ulemons Jan 29, 2026
82d2bec
fix: lint
ulemons Jan 29, 2026
967b254
fix: add clear selection
ulemons Jan 29, 2026
1ee6833
fix: simplify code
ulemons Jan 29, 2026
3d02eef
fix: add safety timeouts
ulemons Jan 29, 2026
4da6e24
fix: add safety timeouts
ulemons Jan 29, 2026
0b6d7e1
fix: test with refetch
ulemons Jan 29, 2026
8d0ddf2
fix: test with refetch
ulemons Jan 29, 2026
55b67b6
fix: test with refetch
ulemons Jan 29, 2026
22a9b8a
fix: lint
ulemons Jan 29, 2026
da0175c
fix: refresh stale
ulemons Jan 29, 2026
f8ac14f
fix: add await
ulemons Jan 29, 2026
ff261b8
fix: add logs
ulemons Jan 29, 2026
dc3d137
fix: refetch all
ulemons Jan 29, 2026
8fb6e78
fix: reset invalid + refetch
ulemons Jan 29, 2026
dae15ee
fix: reset invalid + refetch + all
ulemons Jan 29, 2026
b0feb13
fix: add logs with mexed approach
ulemons Jan 29, 2026
4491049
fix: bypass backend cache
ulemons Jan 29, 2026
cc38043
fix: revert approach with backend cache bypass
ulemons Jan 29, 2026
1dc5ca5
fix: adding logs
ulemons Jan 29, 2026
7ced05f
fix: adding logs
ulemons Jan 29, 2026
1e66a59
fix: modify cache page
ulemons Jan 29, 2026
b663f39
fix: modify cache page
ulemons Jan 29, 2026
3814b92
fix: apply the cache logic to the list
ulemons Jan 29, 2026
52fccb7
fix: refetch queries
ulemons Jan 29, 2026
16c05b3
fix: invalidate backend cache too
ulemons Jan 30, 2026
9ef3a93
fix: remove cachebust flag
ulemons Jan 30, 2026
c8ecaa4
fix: remove logs and useles params
ulemons Jan 30, 2026
c44877a
feat: add currentAttribute in isBot
ulemons Jan 30, 2026
6aef904
feat: add currentAttribute in isBot
ulemons Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion backend/src/api/member/memberFind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.memberRead)

const segmentId = req.query.segments?.length > 0 ? req.query.segments[0] : null
const includeAllAttributes =
req.query.includeAllAttributes === 'true' || req.query.includeAllAttributes === true

if (!segmentId) {
await req.responseHandler.error(req, res, {
code: 400,
Expand All @@ -28,7 +31,12 @@ export default async (req, res) => {
return
}

const payload = await new MemberService(req).findById(req.params.id, segmentId, req.query.include)
const payload = await new MemberService(req).findById(
req.params.id,
segmentId,
req.query.include,
includeAllAttributes,
)

await req.responseHandler.success(req, res, payload)
}
5 changes: 4 additions & 1 deletion backend/src/api/member/memberUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import PermissionChecker from '../../services/user/permissionChecker'
export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.memberEdit)

const payload = await new MemberService(req).update(req.params.id, req.body, {
const { invalidateCache, ...data } = req.body

const payload = await new MemberService(req).update(req.params.id, data, {
syncToOpensearch: true,
manualChange: true,
invalidateCache: invalidateCache ?? false,
})

await req.responseHandler.success(req, res, payload)
Expand Down
3 changes: 3 additions & 0 deletions backend/src/database/repositories/memberRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ class MemberRepository {
segmentId?: string
} = {},
include: Record<string, boolean> = {},
includeAllAttributes = false,
) {
let memberResponse = null

Expand All @@ -1201,6 +1202,7 @@ class MemberRepository {
limit: 1,
offset: 0,
segmentId,
includeAllAttributes,
include: {
memberOrganizations: false,
lfxMemberships: true,
Expand All @@ -1219,6 +1221,7 @@ class MemberRepository {
filter: { id: { eq: id } },
limit: 1,
offset: 0,
includeAllAttributes,
include: {
lfxMemberships: true,
segments: true,
Expand Down
33 changes: 27 additions & 6 deletions backend/src/services/memberService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ export default class MemberService extends LoggerBase {
await SequelizeRepository.commitTransaction(tx)

// Invalidate member query cache after unmerge
await this.invalidateMemberQueryCache([memberId, secondaryMember.id])
await this.invalidateMemberQueryCache([memberId, secondaryMember.id], true)

return { member, secondaryMember }
}),
Expand Down Expand Up @@ -1237,9 +1237,11 @@ export default class MemberService extends LoggerBase {
{
syncToOpensearch = true,
manualChange = false,
invalidateCache = false,
}: {
syncToOpensearch?: boolean
manualChange?: boolean
invalidateCache?: boolean
} = {},
) {
let transaction
Expand All @@ -1260,7 +1262,8 @@ export default class MemberService extends LoggerBase {
await SequelizeRepository.commitTransaction(transaction)

// Invalidate member query cache after update
await this.invalidateMemberQueryCache([id])
// Pass invalidateCache from options to control whether to clear list caches
await this.invalidateMemberQueryCache([id], invalidateCache)

const commonMemberService = new CommonMemberService(
optionsQx(this.options),
Expand Down Expand Up @@ -1315,7 +1318,8 @@ export default class MemberService extends LoggerBase {
await SequelizeRepository.commitTransaction(transaction)

// Invalidate member query cache after bulk delete
await this.invalidateMemberQueryCache(ids)
// Pass invalidateAll=true to also clear list caches since deletion affects list views
await this.invalidateMemberQueryCache(ids, true)
} catch (error) {
await SequelizeRepository.rollbackTransaction(transaction)
throw error
Expand Down Expand Up @@ -1343,6 +1347,10 @@ export default class MemberService extends LoggerBase {
}

await SequelizeRepository.commitTransaction(transaction)

// Invalidate member query cache after deletion
// Pass invalidateAll=true to also clear list caches since deletion affects list views
await this.invalidateMemberQueryCache(ids, true)
} catch (error) {
await SequelizeRepository.rollbackTransaction(transaction)
throw error
Expand All @@ -1353,14 +1361,20 @@ export default class MemberService extends LoggerBase {
}
}

async findById(id, segmentId?: string, include: Record<string, boolean> = {}) {
async findById(
id,
segmentId?: string,
include: Record<string, boolean> = {},
includeAllAttributes = false,
) {
return MemberRepository.findById(
id,
this.options,
{
segmentId,
},
include,
includeAllAttributes,
)
}

Expand Down Expand Up @@ -1465,16 +1479,23 @@ export default class MemberService extends LoggerBase {
return fetchMemberBotSuggestionsBySegment(qx, segmentId, args.limit ?? 10, args.offset ?? 0)
}

async invalidateMemberQueryCache(memberIds?: string[]): Promise<void> {
async invalidateMemberQueryCache(memberIds?: string[], invalidateAll = false): Promise<void> {
try {
const cache = new MemberQueryCache(this.options.redis)

if (memberIds && memberIds.length > 0) {
// Invalidate specific member cache entries
// Invalidate specific member cache entries (queries with filter.id.eq)
for (const memberId of memberIds) {
await cache.invalidateByPattern(`members_advanced:${memberId}:*`)
}
this.log.debug(`Invalidated member query cache for ${memberIds.length} specific members`)

// Only invalidate all caches if explicitly requested
// This is useful for operations like update/delete that affect list views
if (invalidateAll) {
await cache.invalidateAll()
this.log.debug('Invalidated all member query cache entries')
}
} else {
// Invalidate all cache entries
await cache.invalidateAll()
Expand Down
153 changes: 143 additions & 10 deletions frontend/src/modules/member/components/list/member-list-toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
<lf-icon :name="markAsTeamMemberOptions.icon" :size="20" class="mr-1" />
{{ markAsTeamMemberOptions.copy }}
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermission(LfPermission.memberEdit)"
:command="{
action: 'markAsBot',
value: markAsBotOptions.value,
}"
>
<lf-icon :name="markAsBotOptions.icon" :size="20" class="mr-1" />
{{ markAsBotOptions.copy }}
</el-dropdown-item>
<el-dropdown-item
v-if="hasPermission(LfPermission.memberEdit)"
:command="{ action: 'editAttribute' }"
Expand All @@ -60,7 +70,6 @@

<app-bulk-edit-attribute-popover
v-model="bulkAttributesUpdateVisible"
@reload="fetchMembers({ reload: true })"
/>
</template>

Expand All @@ -70,6 +79,7 @@ import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import pluralize from 'pluralize';
import { useMemberStore } from '@/modules/member/store/pinia';
import { useLfSegmentsStore } from '@/modules/lf/segments/store';
import { MemberService } from '@/modules/member/member-service';
import ConfirmDialog from '@/shared/dialog/confirm-dialog';

Expand All @@ -85,22 +95,46 @@ import { EventType, FeatureEventKey } from '@/shared/modules/monitoring/types/ev
import LfIcon from '@/ui-kit/icon/Icon.vue';
import LfButton from '@/ui-kit/button/Button.vue';
import LfTableBulkActions from '@/ui-kit/table/table-bulk-actions.vue';
import { useQueryClient } from '@tanstack/vue-query';
import { TanstackKey } from '@/shared/types/tanstack';

const { trackEvent } = useProductTracking();

const route = useRoute();
const queryClient = useQueryClient();

const authStore = useAuthStore();
const { getUser } = authStore;

const memberStore = useMemberStore();
const { selectedMembers, filters } = storeToRefs(memberStore);
const { fetchMembers } = memberStore;

const { hasPermission } = usePermissions();

const bulkAttributesUpdateVisible = ref(false);

// Refresh member data by invalidating TanStack Query cache
// Note: Backend cache is invalidated by passing invalidateCache parameter to update/delete operations
const refreshMemberData = async () => {
await queryClient.invalidateQueries({
queryKey: [TanstackKey.MEMBERS_LIST],
});
};

// Helper function to fetch member with all attributes before bulk update
const fetchMemberWithAllAttributes = async (memberId) => {
const lsSegmentsStore = useLfSegmentsStore();
const { selectedProjectGroup } = storeToRefs(lsSegmentsStore);

const response = await MemberService.find(
memberId,
selectedProjectGroup.value?.id,
true,
);

return response;
};

const markAsTeamMemberOptions = computed(() => {
const isTeamView = filters.value.settings.teamMember === 'filter';
const membersCopy = pluralize(
Expand All @@ -124,6 +158,31 @@ const markAsTeamMemberOptions = computed(() => {
};
});

const markAsBotOptions = computed(() => {
const membersCopy = pluralize(
'person',
selectedMembers.value.length,
false,
);

// Check if any of the selected members is already marked as bot
const hasBot = selectedMembers.value.some((member) => member.attributes?.isBot?.default);

if (hasBot) {
return {
icon: 'robot',
copy: 'Unmark as bot',
value: false,
};
}

return {
icon: 'robot',
copy: 'Mark as bot',
value: true,
};
});

const handleMergeMembers = async () => {
const [firstMember, secondMember] = selectedMembers.value;

Expand Down Expand Up @@ -168,7 +227,13 @@ const doDestroyAllWithConfirm = () => ConfirmDialog({
const ids = selectedMembers.value.map((m) => m.id);
return MemberService.destroyAll(ids);
})
.then(() => fetchMembers({ reload: true }));
.then(async () => {
// Clear selection immediately to prevent UI issues
selectedMembers.value = [];

// Refresh data to ensure UI is up to date
await refreshMemberData();
});

const handleDoExport = async () => {
const ids = selectedMembers.value.map((i) => i.id);
Expand Down Expand Up @@ -226,20 +291,77 @@ const handleEditAttribute = async () => {
const doMarkAsTeamMember = async (value) => {
ToastStore.info('People are being updated');

return Promise.all(selectedMembers.value.map((member) => MemberService.update(member.id, {
attributes: {
...member.attributes,
const updatePromises = selectedMembers.value.map(async (member) => {
// Fetch member with all attributes to prevent data loss
const memberWithAllAttributes = await fetchMemberWithAllAttributes(member.id);
const currentAttributes = memberWithAllAttributes.attributes;

const updatedAttributes = {
...currentAttributes,
isTeamMember: {
default: value,
custom: value,
},
},
}, member.segmentIds)))
.then(() => {
};

return MemberService.update(member.id, {
attributes: updatedAttributes,
invalidateCache: true,
});
});

return Promise.all(updatePromises)
.then(async () => {
ToastStore.closeAll();
ToastStore.success(`${
pluralize('Person', selectedMembers.value.length, true)} updated successfully`);

// Clear selection immediately to prevent UI issues
selectedMembers.value = [];

// Refresh data to ensure UI is up to date
await refreshMemberData();
})
.catch(() => {
ToastStore.closeAll();
ToastStore.error('Error updating people');
});
};

const doMarkAsBot = async (value) => {
ToastStore.info('People are being updated');

const updatePromises = selectedMembers.value.map(async (member) => {
// Fetch member with all attributes to prevent data loss
const memberWithAllAttributes = await fetchMemberWithAllAttributes(member.id);
const currentAttributes = memberWithAllAttributes.attributes;

const updatedAttributes = {
...currentAttributes,
isBot: {
...currentAttributes.isBot,
default: value,
custom: value,
},
};

return MemberService.update(member.id, {
attributes: updatedAttributes,
invalidateCache: true,
});
});

return Promise.all(updatePromises)
.then(async () => {
ToastStore.closeAll();
ToastStore.success(`${
pluralize('Person', selectedMembers.value.length, true)} updated successfully`);

fetchMembers({ reload: true });
// Clear selection immediately to prevent UI issues
selectedMembers.value = [];

// Refresh data to ensure UI is up to date
await refreshMemberData();
})
.catch(() => {
ToastStore.closeAll();
Expand All @@ -259,6 +381,17 @@ const handleCommand = async (command) => {
});

await doMarkAsTeamMember(command.value);
} else if (command.action === 'markAsBot') {
trackEvent({
key: FeatureEventKey.MARK_AS_BOT,
type: EventType.FEATURE,
properties: {
path: route.path,
bulk: true,
},
});

await doMarkAsBot(command.value);
} else if (command.action === 'export') {
await handleDoExport();
} else if (command.action === 'mergeMembers') {
Expand Down
Loading
Loading