Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion models/contact/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,8 @@ export function createModel (builder: Builder): void {
mode: ['context'],
group: 'remove'
},
secured: true
secured: true,
visibilityTester: contact.function.CanKickEmployee
},
contact.action.KickEmployee
)
Expand Down
4 changes: 3 additions & 1 deletion plugins/contact-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
"build": "compile ui",
"build:docs": "api-extractor run --local",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"svelte-check": "do-svelte-check",
"_phase:svelte-check": "do-svelte-check",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate"
"_phase:validate": "compile validate",
"_phase:test": "jest --passWithNoTests --silent"
},
"devDependencies": {
"svelte-loader": "^3.2.0",
Expand Down
61 changes: 61 additions & 0 deletions plugins/contact-resources/src/__tests__/kickEmployee.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

import type { Person } from '@hcengineering/contact'
import type { AccountUuid, Ref } from '@hcengineering/core'
import { canKick, isCurrentUser } from '../kickEmployee'

const me = 'account-me' as AccountUuid
const myPerson = 'person-me' as Ref<Person>

const self = { _id: myPerson, personUuid: me }
const other = { _id: 'person-other' as Ref<Person>, personUuid: 'account-other' as AccountUuid }

describe('isCurrentUser', () => {
it('detects the current user by account uuid', () => {
expect(isCurrentUser({ _id: 'x' as Ref<Person>, personUuid: me }, me, myPerson)).toBe(true)
})

it('detects the current user by employee ref', () => {
expect(isCurrentUser({ _id: myPerson }, me, myPerson)).toBe(true)
})

it('returns false for another person', () => {
expect(isCurrentUser(other, me, myPerson)).toBe(false)
})

it('returns false when the current user is unknown', () => {
expect(isCurrentUser({ _id: myPerson }, undefined, undefined)).toBe(false)
})
})

describe('canKick', () => {
it('allows kicking another employee', () => {
expect(canKick(other, me, myPerson)).toBe(true)
})

it('does not allow kicking yourself', () => {
expect(canKick(self, me, myPerson)).toBe(false)
})

it('does not allow a selection that contains yourself', () => {
expect(canKick([other, self], me, myPerson)).toBe(false)
})

it('returns false for an empty or missing selection', () => {
expect(canKick([], me, myPerson)).toBe(false)
expect(canKick(undefined, me, myPerson)).toBe(false)
})
})
12 changes: 10 additions & 2 deletions plugins/contact-resources/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
getCurrentEmployee,
getGravatarUrl,
getName,
type AvatarInfo,
Expand All @@ -32,7 +33,8 @@ import {
type DocumentQuery,
type Ref,
type RelatedDocument,
type WithLookup
type WithLookup,
getCurrentAccount
} from '@hcengineering/core'
import login from '@hcengineering/login'
import { getResource, type IntlString, type Resources } from '@hcengineering/platform'
Expand Down Expand Up @@ -130,7 +132,8 @@ import PersonPreviewProvider from './components/person/PersonPreviewProvider.sve
import TranslationSettings from './components/TranslationSettings.svelte'

import { get } from 'svelte/store'
import { canMergePersons, canResendInvitation } from './visibilityTester'
import { canKickEmployee, canMergePersons, canResendInvitation } from './visibilityTester'
import { isCurrentUser } from './kickEmployee'
import contact from './plugin'
import {
channelIdentifierProvider,
Expand Down Expand Up @@ -301,6 +304,10 @@ async function resendInvite (doc: Person): Promise<void> {
}

async function kickEmployee (doc: Person): Promise<void> {
if (isCurrentUser(doc, getCurrentAccount().uuid, getCurrentEmployee())) {
// Kicking yourself would lock you out of the workspace, use "Leave workspace" in the profile instead.
return
}
showPopup(MessageBox, {
label: contact.string.KickEmployee,
message: contact.string.KickEmployeeDescr,
Expand Down Expand Up @@ -471,6 +478,7 @@ export default async (): Promise<Resources> => ({
ChannelIdentifierProvider: channelIdentifierProvider,
CanResendInvitation: canResendInvitation,
CanMergePersons: canMergePersons,
CanKickEmployee: canKickEmployee,
HideInactive: hideInactive
},
resolver: {
Expand Down
43 changes: 43 additions & 0 deletions plugins/contact-resources/src/kickEmployee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

import type { Person } from '@hcengineering/contact'
import type { AccountUuid, Ref } from '@hcengineering/core'

/**
* Returns true if the given person is the currently logged in user.
* Used to prevent kicking yourself out of the workspace; leaving is done from the profile settings.
*/
export function isCurrentUser (
person: Pick<Person, '_id' | 'personUuid'>,
currentAccount: AccountUuid | undefined,
currentEmployee: Ref<Person> | undefined
): boolean {
if (currentAccount !== undefined && person.personUuid === currentAccount) return true
return currentEmployee !== undefined && person._id === currentEmployee
}

/**
* Kick is available only for a single employee which is not the current user.
*/
export function canKick (
doc: Pick<Person, '_id' | 'personUuid'> | Array<Pick<Person, '_id' | 'personUuid'>> | undefined,
currentAccount: AccountUuid | undefined,
currentEmployee: Ref<Person> | undefined
): boolean {
if (doc === undefined) return false
const docs = Array.isArray(doc) ? doc : [doc]
return docs.length > 0 && !docs.some((it) => isCurrentUser(it, currentAccount, currentEmployee))
}
3 changes: 2 additions & 1 deletion plugins/contact-resources/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export default mergeIds(contactId, contact, {
FilterChannelHasNewMessagesResult: '' as FilterFunction,
PersonTooltipProvider: '' as Resource<(client: Client, doc?: Doc | null) => Promise<LabelAndProps | undefined>>,
CanResendInvitation: '' as Resource<ViewActionAvailabilityFunction>,
CanMergePersons: '' as Resource<ViewActionAvailabilityFunction>
CanMergePersons: '' as Resource<ViewActionAvailabilityFunction>,
CanKickEmployee: '' as Resource<ViewActionAvailabilityFunction>
},
aggregation: {
CreatePersonAggregationManager: '' as CreateAggregationManagerFunc,
Expand Down
8 changes: 7 additions & 1 deletion plugins/contact-resources/src/visibilityTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
// limitations under the License.
//

import contact, { type Person, type Employee } from '@hcengineering/contact'
import contact, { getCurrentEmployee, type Person, type Employee } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { canKick } from './kickEmployee'

export async function canResendInvitation (employee: Employee): Promise<boolean> {
return !employee.active
Expand All @@ -27,3 +29,7 @@ export async function canMergePersons (person: Person): Promise<boolean> {

return hierarchy.hasMixin(person, contact.mixin.Employee)
}

export async function canKickEmployee (employee: Employee | Employee[] | undefined): Promise<boolean> {
return canKick(employee, getCurrentAccount().uuid, getCurrentEmployee())
}