UBERF-9732: Use huly id as primary social id (#8499)

* uberf-9732: use huly id as primary social id
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* uberf-9732: fix person id filter
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev
2025-04-09 09:52:06 +07:00
committed by GitHub
parent 420b31d66f
commit 4cb1f3e411
29 changed files with 478 additions and 238 deletions
@@ -0,0 +1,202 @@
<!--
// Copyright © 2023 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.
-->
<script lang="ts">
import contact, { Person, SocialIdentity } from '@hcengineering/contact'
import core, { FindResult, getObjectValue, includesAny, PersonId, Ref, Space, WithLookup } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import ui, {
deviceOptionsStore,
EditWithIcon,
Icon,
IconCheck,
IconSearch,
Loading,
resizeObserver
} from '@hcengineering/ui'
import view, { Filter } from '@hcengineering/view'
import { FILTER_DEBOUNCE_MS, sortFilterValues } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import PersonPresenter from './PersonPresenter.svelte'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
export let onChange: (e: Filter) => void
const client = getClient()
filter.modes = filter.modes === undefined ? [view.filter.FilterObjectIn, view.filter.FilterObjectNin] : filter.modes
filter.mode = filter.mode === undefined ? filter.modes[0] : filter.mode
let socialIdentities: (WithLookup<SocialIdentity> | undefined | null)[] = []
let socialIdentitiesPromise: Promise<FindResult<SocialIdentity>> | undefined
let personIdToPersonMap: Record<PersonId, Ref<Person>> = {}
let personToPersonIdsMap: Record<Ref<Person>, PersonId[]> = {}
let persons: Ref<Person>[] = []
const targets = new Set<any>()
let filterUpdateTimeout: any | undefined
let search: string = ''
const dispatch = createEventDispatcher()
async function getValues (search: string): Promise<void> {
if (socialIdentitiesPromise !== undefined) {
await socialIdentitiesPromise
}
targets.clear()
const spaces = (
await client.findAll(core.class.Space, { archived: true }, { projection: { _id: 1, archived: 1, _class: 1 } })
).map((it) => it._id)
const baseObjects = await client.findAll(
filter.key._class,
space !== undefined ? { space } : { space: { $nin: spaces } },
{
projection: { [filter.key.key]: 1, space: 1 }
}
)
for (const object of baseObjects) {
const socialIdentity = getObjectValue(filter.key.key, object) ?? undefined
targets.add(socialIdentity)
}
for (const object of filter.value) {
targets.add(object)
}
const resultQuery =
search !== ''
? {
'$lookup.attachedTo.name': { $like: '%' + search + '%' },
_id: { $in: Array.from(targets.keys()) }
}
: {
_id: { $in: Array.from(targets.keys()) }
}
socialIdentitiesPromise = client.findAll(contact.class.SocialIdentity, resultQuery, {
lookup: { attachedTo: contact.class.Person }
})
socialIdentities = await socialIdentitiesPromise
if (targets.has(undefined)) {
socialIdentities.unshift(undefined)
}
personIdToPersonMap = (socialIdentities ?? []).reduce<Record<string, Ref<Person>>>((acc, sid) => {
if (sid == null) return acc
const person = sid?.$lookup?.attachedTo
if (person == null) return acc
acc[sid._id] = person._id
return acc
}, {})
personToPersonIdsMap = (socialIdentities ?? []).reduce<Record<Ref<Person>, PersonId[]>>((acc, sid) => {
if (sid == null) return acc
const person = sid?.$lookup?.attachedTo
if (person == null) return acc
if (acc[person._id] == null) {
acc[person._id] = []
}
acc[person._id].push(sid._id)
return acc
}, {})
persons = sortFilterValues(Array.from(new Set(Object.values(personIdToPersonMap))), (p) =>
isPersonSelected(p, filter.value)
)
socialIdentitiesPromise = undefined
}
function isPersonSelected (person: Ref<Person>, selectedIds: any[]): boolean {
const personSocialIds = personToPersonIdsMap[person] ?? []
return includesAny(personSocialIds, selectedIds)
}
function handleFilterToggle (person: Ref<Person>): void {
const personSocialIds = personToPersonIdsMap[person] ?? []
if (isPersonSelected(person, filter.value)) {
filter.value = filter.value.filter((p) => !personSocialIds.includes(p))
} else {
filter.value = [...filter.value, ...personSocialIds]
}
updateFilter()
}
function updateFilter (): void {
clearTimeout(filterUpdateTimeout)
filterUpdateTimeout = setTimeout(() => {
onChange(filter)
}, FILTER_DEBOUNCE_MS)
}
$: {
void getValues(search)
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
<div class="header">
<EditWithIcon
icon={IconSearch}
size={'large'}
width={'100%'}
autoFocus={!$deviceOptionsStore.isMobile}
bind:value={search}
placeholder={presentation.string.Search}
/>
</div>
<div class="scroll">
<div class="box">
{#if socialIdentitiesPromise}
<Loading />
{:else}
{#each persons as person}
<button
class="menu-item no-focus flex-row-center"
on:click={() => {
handleFilterToggle(person)
}}
>
<div class="clear-mins flex-grow">
<PersonPresenter
value={person}
shouldShowPlaceholder
defaultName={ui.string.NotSelected}
disabled
noUnderline
/>
</div>
<div class="check pointer-events-none">
{#if isPersonSelected(person, filter.value)}
<Icon icon={IconCheck} size={'small'} />
{/if}
</div>
</button>
{/each}
{/if}
</div>
</div>
<div class="menu-space" />
</div>
+7 -3
View File
@@ -25,6 +25,7 @@ import {
import {
AccountRole,
SocialIdType,
type AccountUuid,
type Class,
type Client,
type Data,
@@ -102,6 +103,7 @@ import PersonFilterValuePresenter from './components/PersonFilterValuePresenter.
import PersonEditor from './components/PersonEditor.svelte'
import PersonIcon from './components/PersonIcon.svelte'
import PersonPresenter from './components/PersonPresenter.svelte'
import PersonIdFilter from './components/PersonIdFilter.svelte'
import PersonRefPresenter from './components/PersonRefPresenter.svelte'
import SelectAvatars from './components/SelectAvatars.svelte'
import SelectUsersPopup from './components/SelectUsersPopup.svelte'
@@ -199,7 +201,8 @@ export {
UserDetails,
UserInfo,
UsersList,
UsersPopup
UsersPopup,
PersonIdFilter
}
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
@@ -299,7 +302,7 @@ async function kickEmployee (doc: Person): Promise<void> {
if (doc.personUuid != null) {
const leaveWorkspace = await getResource(login.function.LeaveWorkspace)
await leaveWorkspace(doc.personUuid)
await leaveWorkspace(doc.personUuid as AccountUuid)
}
}
})
@@ -390,7 +393,8 @@ export default async (): Promise<Resources> => ({
EditOrganizationPanel,
ChannelIcon,
SpaceMembersEditor,
ContactNamePresenter
ContactNamePresenter,
PersonIdFilter
},
completion: {
EmployeeQuery: async (
+2 -1
View File
@@ -238,7 +238,8 @@ export const contactPlugin = plugin(contactId, {
CreateGuest: '' as AnyComponent,
SpaceMembersEditor: '' as AnyComponent,
ContactNamePresenter: '' as AnyComponent,
PersonFilterValuePresenter: '' as AnyComponent
PersonFilterValuePresenter: '' as AnyComponent,
PersonIdFilter: '' as AnyComponent
},
channelProvider: {
Email: '' as Ref<ChannelProvider>,
+3 -17
View File
@@ -29,6 +29,7 @@ import {
MeasureContext,
notEmpty,
PersonId,
pickPrimarySocialId,
Ref,
SocialId,
toIdMap,
@@ -283,15 +284,6 @@ export function formatContactName (
return name
}
// TODO: remove me in favor of the same util in core package
export function pickPrimarySocialId (ids: PersonId[]): PersonId {
if (ids.length === 0) {
throw new Error('No social ids provided')
}
return ids[0]
}
export function includesAny (members: PersonId[], ids: PersonId[]): boolean {
return members.some((m) => ids.includes(m))
}
@@ -337,13 +329,13 @@ export async function getPersonRefsBySocialIds (
}
export async function getPrimarySocialId (client: Client, person: Ref<Person>): Promise<PersonId | undefined> {
const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: person })
const socialIds = await client.findAll(contact.class.SocialIdentity, { attachedTo: person, verifiedOn: { $gt: 0 } })
if (socialIds.length === 0) {
return
}
return pickPrimarySocialId(socialIds.map((it) => it._id))
return pickPrimarySocialId(socialIds)._id
}
export async function getAllSocialStringsByPersonId (client: Client, personId: PersonId): Promise<PersonId[]> {
@@ -393,12 +385,6 @@ export async function getAllAccounts (client: Client): Promise<AccountUuid[]> {
return employees.map((it) => it.personUuid).filter(notEmpty)
}
export async function getAllEmployeesPrimarySocialStrings (client: Client): Promise<PersonId[]> {
const socialStringsByPerson = getSocialStringsByEmployee(client)
return Object.values(socialStringsByPerson).map((it) => pickPrimarySocialId(it))
}
export async function getAllUserAccounts (client: Client): Promise<AccountUuid[]> {
const employees = await client.findAll(contact.mixin.Employee, { active: true })
+2 -1
View File
@@ -27,6 +27,7 @@ import {
AccountRole,
concatLink,
parseSocialIdString,
type AccountUuid,
type Person,
type WorkspaceInfoWithStatus,
type WorkspaceUserOperation
@@ -661,7 +662,7 @@ export async function changeUsername (first: string, last: string): Promise<void
}
}
export async function leaveWorkspace (account: string): Promise<LoginInfo | null> {
export async function leaveWorkspace (account: AccountUuid): Promise<LoginInfo | null> {
return await getAccountClient().leaveWorkspace(account)
}
@@ -0,0 +1,26 @@
<!--
// Copyright © 2023 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.
-->
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import { Component } from '@hcengineering/ui'
import contact from '@hcengineering/contact'
import { Filter } from '@hcengineering/view'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
export let onChange: (e: Filter) => void
</script>
<Component is={contact.component.PersonIdFilter} props={{ filter, space, onChange }} />
@@ -18,17 +18,20 @@
import { getClient } from '@hcengineering/presentation'
import contact, { getPersonRefsBySocialIds, Person } from '@hcengineering/contact'
export let value: [any, PersonId][]
export let value: PersonId[]
const client = getClient()
let persons: Ref<Person>[] = []
$: void getPersons(value.map((p) => p[1]).flat())
$: void getPersons(value)
async function getPersons (ids: PersonId[]): Promise<void> {
if (ids !== undefined) {
const personsMap = await getPersonRefsBySocialIds(client, ids)
persons = Object.values(personsMap)
console.log(persons)
if (ids.length === 0) {
persons = []
} else {
const personsMap = await getPersonRefsBySocialIds(client, ids)
persons = Object.values(personsMap)
}
}
}
</script>
+3
View File
@@ -98,6 +98,7 @@ import StatusPresenter from './components/status/StatusPresenter.svelte'
import StatusRefPresenter from './components/status/StatusRefPresenter.svelte'
import PersonArrayEditor from './components/PersonArrayEditor.svelte'
import PersonIdPresenter from './components/PersonIdPresenter.svelte'
import PersonIdFilter from './components/filter/PersonIdFilter.svelte'
import PersonIdFilterValuePresenter from './components/filter/PersonIdFilterValuePresenter.svelte'
import AudioViewer from './components/viewer/AudioViewer.svelte'
import ImageViewer from './components/viewer/ImageViewer.svelte'
@@ -176,6 +177,7 @@ export { default as NavLink } from './components/navigator/NavLink.svelte'
export { default as StatusPresenter } from './components/status/StatusPresenter.svelte'
export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte'
export { default as PersonIdPresenter } from './components/PersonIdPresenter.svelte'
export { default as PersonIdFilter } from './components/filter/PersonIdFilter.svelte'
export { default as PersonIdFilterValuePresenter } from './components/filter/PersonIdFilterValuePresenter.svelte'
export { default as FoldersBrowser } from './components/folders/FoldersBrowser.svelte'
export { default as RelationsEditor } from './components/RelationsEditor.svelte'
@@ -309,6 +311,7 @@ export default async (): Promise<Resources> => ({
StatusRefPresenter,
PersonArrayEditor,
PersonIdPresenter,
PersonIdFilter,
PersonIdFilterValuePresenter,
DateFilterPresenter,
StringFilterPresenter,
+1
View File
@@ -175,6 +175,7 @@ const view = plugin(viewId, {
SearchSelector: '' as AnyComponent,
FoldersBrowser: '' as AnyComponent,
PersonIdPresenter: '' as AnyComponent,
PersonIdFilter: '' as AnyComponent,
RolePresenter: '' as AnyComponent
},
ids: {