UBERF-9716: New profile preview and initial achievements (#8504)

* UBERF-9716: Draft user profile

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Redesign user profile card

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Hide error for profile popup

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Add achievement header and refactor

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Display timezone

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix formatting

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Refactor

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Add preview for mentions

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Move achievements to separate module and use component extension

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix rush check

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix validate

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix validate

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Minor fixes

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix formatting and template

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Update npm lock

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Add light background and fix some issues

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix svelte error

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Add preview to new chat

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix svelte

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Update avatar selector

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Use instead of px

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Accent name

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Fix formatting

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Remove unused width prop

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-9716: Remove isButtonIcon

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2025-04-15 13:13:16 +07:00
committed by GitHub
parent 7953ac0951
commit d2603d8629
108 changed files with 2272 additions and 643 deletions
+688 -352
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -234,6 +234,9 @@
"@hcengineering/inbox": "^0.6.0",
"@hcengineering/inbox-assets": "^0.6.0",
"@hcengineering/inbox-resources": "^0.6.0",
"@hcengineering/achievement": "^0.6.0",
"@hcengineering/achievement-assets": "^0.6.0",
"@hcengineering/achievement-resources": "^0.6.0",
"electron-squirrel-startup": "~1.0.0",
"dotenv": "~16.0.0",
"electron-context-menu": "^4.0.4",
+4
View File
@@ -64,6 +64,7 @@ import workbench, { workbenchId } from '@hcengineering/workbench'
import { mailId } from '@hcengineering/mail'
import { chatId } from '@hcengineering/chat'
import { inboxId } from '@hcengineering/inbox'
import { achievementId } from '@hcengineering/achievement'
import '@hcengineering/activity-assets'
import '@hcengineering/analytics-collector-assets'
@@ -113,6 +114,7 @@ import '@hcengineering/workbench-assets'
import '@hcengineering/mail-assets'
import '@hcengineering/chat-assets'
import '@hcengineering/inbox-assets'
import '@hcengineering/achievement-assets'
import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector'
import { coreId } from '@hcengineering/core'
@@ -224,6 +226,7 @@ function configureI18n (): void {
addStringsLoader(chatId, async (lang: string) => await import(`@hcengineering/chat-assets/lang/${lang}.json`))
addStringsLoader(inboxId, async (lang: string) => await import(`@hcengineering/inbox-assets/lang/${lang}.json`))
addStringsLoader(processId, async (lang: string) => await import(`@hcengineering/process-assets/lang/${lang}.json`))
addStringsLoader(achievementId, async (lang: string) => await import(`@hcengineering/achievement-assets/lang/${lang}.json`))
}
export async function configurePlatform (): Promise<void> {
@@ -361,6 +364,7 @@ export async function configurePlatform (): Promise<void> {
addLocation(inboxId, () => import(/* webpackChunkName: "inbox" */ '@hcengineering/inbox-resources'))
addLocation(mailId, () => import(/* webpackChunkName: "card" */ '@hcengineering/mail-resources'))
addLocation(processId, () => import(/* webpackChunkName: "process" */ '@hcengineering/process-resources'))
addLocation(achievementId, () => import(/* webpackChunkName: "achievement" */ '@hcengineering/achievement-resources'))
setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
+3
View File
@@ -54,6 +54,9 @@
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"@hcengineering/achievement": "^0.6.0",
"@hcengineering/achievement-assets": "^0.6.0",
"@hcengineering/achievement-resources": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-assets": "^0.6.3",
"@hcengineering/activity-resources": "^0.6.1",
+4
View File
@@ -73,6 +73,7 @@ import { chatId } from '@hcengineering/chat'
import github, { githubId } from '@hcengineering/github'
import { bitrixId } from '@hcengineering/bitrix'
import {inboxId} from '@hcengineering/inbox'
import {achievementId} from '@hcengineering/achievement'
import '@hcengineering/activity-assets'
import '@hcengineering/analytics-collector-assets'
@@ -124,6 +125,7 @@ import '@hcengineering/chat-assets'
import '@hcengineering/inbox-assets'
import '@hcengineering/mail-assets'
import '@hcengineering/github-assets'
import '@hcengineering/achievement-assets'
import { coreId } from '@hcengineering/core'
import presentation, {
@@ -355,6 +357,7 @@ function configureI18n(): void {
addStringsLoader(chatId, async (lang: string) => await import(`@hcengineering/chat-assets/lang/${lang}.json`))
addStringsLoader(processId, async (lang: string) => await import(`@hcengineering/process-assets/lang/${lang}.json`))
addStringsLoader(inboxId, async (lang: string) => await import(`@hcengineering/inbox-assets/lang/${lang}.json`))
addStringsLoader(achievementId, async (lang: string) => await import(`@hcengineering/achievement-assets/lang/${lang}.json`))
}
export async function configurePlatform() {
@@ -554,6 +557,7 @@ export async function configurePlatform() {
addLocation(chatId, () => import(/* webpackChunkName: "chat" */ '@hcengineering/chat-resources'))
addLocation(processId, () => import(/* webpackChunkName: "process" */ '@hcengineering/process-resources'))
addLocation(inboxId, () => import(/* webpackChunkName: "inbox" */ '@hcengineering/inbox-resources'))
addLocation(achievementId, () => import(/* webpackChunkName: "achievement" */ '@hcengineering/achievement-resources'))
setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "model"
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@hcengineering/model-achievement",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Copyright © Hardcore Engineering Inc.",
"template": "@hcengineering/model-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/achievement": "^0.6.0",
"@hcengineering/achievement-resources": "^0.6.0",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/ui": "^0.6.15"
}
}
+37
View File
@@ -0,0 +1,37 @@
//
// Copyright © 2025 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 core from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import contact from '@hcengineering/contact'
import presentation from '@hcengineering/model-presentation'
import achievement from './plugin'
export { default } from './plugin'
export { achievementId } from '@hcengineering/achievement'
export function createModel (builder: Builder): void {
builder.createDoc(
presentation.class.ComponentPointExtension,
core.space.Model,
{
extension: contact.extension.PersonAchievementsPresenter,
component: achievement.component.PersonAchievementsPresenter,
props: {}
},
achievement.extensions.PersonAchievementsPresenter
)
}
+25
View File
@@ -0,0 +1,25 @@
//
// Copyright © 2025 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 { mergeIds } from '@hcengineering/platform'
import { achievementId } from '@hcengineering/achievement'
import achievement from '@hcengineering/achievement-resources/src/plugin'
import { type Ref } from '@hcengineering/core'
import { type ComponentPointExtension } from '@hcengineering/presentation'
export default mergeIds(achievementId, achievement, {
extensions: {
PersonAchievementsPresenter: '' as Ref<ComponentPointExtension>
}
})
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
+2 -1
View File
@@ -121,6 +121,7 @@
"@hcengineering/model-chat": "^0.6.0",
"@hcengineering/model-process": "^0.6.0",
"@hcengineering/model-server-process": "^0.6.0",
"@hcengineering/model-inbox": "^0.6.0"
"@hcengineering/model-inbox": "^0.6.0",
"@hcengineering/model-achievement": "^0.6.0"
}
}
+2
View File
@@ -115,6 +115,7 @@ import { presenceId, createModel as presenceModel } from '@hcengineering/model-p
import { chatId, createModel as chatModel } from '@hcengineering/model-chat'
import processes, { processId, createModel as processModel } from '@hcengineering/model-process'
import { createModel as inboxModel, inboxId } from '@hcengineering/model-inbox'
import { achievementId, createModel as achievementModel } from '@hcengineering/model-achievement'
import { type Plugin } from '@hcengineering/platform'
interface ConfigurablePlugin extends Omit<Data<PluginConfiguration>, 'pluginId' | 'transactions'> {}
@@ -452,6 +453,7 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
[mailModel, mailId],
[chatModel, chatId],
[inboxModel, inboxId],
[achievementModel, achievementId],
[serverCoreModel, serverCoreId],
[serverAttachmentModel, serverAttachmentId],
+4
View File
@@ -892,6 +892,10 @@ export function createModel (builder: Builder): void {
arrayPresenter: contact.component.EmployeeArrayEditor
})
builder.mixin(contact.class.Person, core.class.Class, view.mixin.PreviewPresenter, {
presenter: contact.component.PersonPreviewPresenter
})
builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete]
})
+2 -1
View File
@@ -57,7 +57,8 @@ export default mergeIds(contactId, contact, {
ActivityChannelPresenter: '' as AnyComponent,
EmployeeFilter: '' as AnyComponent,
EmployeeFilterValuePresenter: '' as AnyComponent,
ChannelIcon: '' as AnyComponent
ChannelIcon: '' as AnyComponent,
PersonPreviewPresenter: '' as AnyComponent
},
string: {
SearchEmployee: '' as IntlString,
+14
View File
@@ -123,6 +123,8 @@
--theme-button-contrast-disabled-color: rgba(0, 0, 0, .5);
--theme-button-contrast-border: rgba(255, 255, 255, .2);
--theme-button-container-color: #25262A;
--theme-refinput-divider: rgba(255, 255, 255, .07);
--theme-refinput-border: rgba(255, 255, 255, .1);
@@ -193,6 +195,7 @@
--theme-popup-header: #3A3A47;
--theme-popup-shadow: 0 0 .5rem rgba(0, 0, 0, .2);
--theme-popup-checkicon: #FFFFFF99;
--theme-popup-deactivated: #1A1A28;
--theme-panel-color: #1A1A28;
--theme-calendar-today-color: #fff;
--theme-calendar-holiday-color: #eb5757;
@@ -421,6 +424,8 @@
--theme-button-contrast-disabled-color: rgba(255, 255, 255, .5);
--theme-button-contrast-border: rgba(0, 0, 0, .2);
--theme-button-container-color: #F1F1F1;
--theme-refinput-divider: rgba(0, 0, 0, .07);
--theme-refinput-border: rgba(0, 0, 0, .1);
@@ -497,6 +502,7 @@
--theme-popup-header: #EBEBEB;
--theme-popup-shadow: 0 0 .5rem rgba(0, 0, 0, .2);
--theme-popup-checkicon: #205DC2;
--theme-popup-deactivated: #C7C8CA;
--theme-panel-color: #FFFFFF;
--theme-calendar-today-color: #000;
--theme-calendar-holiday-color: #eb5757;
@@ -680,4 +686,12 @@
--calendar-event-back-color: #f3f6fb;
--calendar-event-border-color: rgba(43, 81, 144, 0.2);
--calendar-event-handle-color: #2b5190;
--theme-button-contrast-color: #fff;
--theme-button-contrast-enabled: rgba(0, 0, 0, .8);
--theme-button-contrast-hovered: #000;
--theme-button-contrast-pressed: rgba(0, 0, 0, .6);
--theme-button-contrast-disabled: rgba(0, 0, 0, .6);
--theme-button-contrast-disabled-color: rgba(255, 255, 255, .5);
--theme-button-contrast-border: rgba(0, 0, 0, .2);
}
+2 -2
View File
@@ -127,7 +127,7 @@
--love-active-call-transform: scaleY(0.25) scaleX(0.4);
--love-active-call-filter: blur(17px);
--global-online-color: #49a26b;
--global-online-color: #209B18;
--global-offline-color: #d1d5de;
}
@@ -223,6 +223,6 @@
--love-active-call-filter: blur(10px);
--love-active-call-transform: scaleY(0.3) scaleX(0.42);
--global-online-color: #49a26b;
--global-online-color: #209B18;
--global-offline-color: #5A667E;
}
+40 -3
View File
@@ -130,10 +130,27 @@
background-color: var(--theme-button-default);
pointer-events: none;
&.clickable {
cursor: pointer;
pointer-events: auto;
& > * {
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
}
&.withStatus {
mask-repeat: no-repeat;
mask-size: cover;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M10,13.5c0-1.9,1.6-3.5,3.5-3.5c1,0,1.9,0.4,2.5,1.1V0H0v16h11.1C10.4,15.4,10,14.5,10,13.5z' /%3E%3C/svg%3E");
&.hulyAvatarSize-large {
mask-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E%3Ccircle cx='69' cy='69' r='11' /%3E%3C/svg%3E"),
linear-gradient(#000 0 0);
mask-composite: exclude;
}
}
&.circle,
@@ -166,7 +183,12 @@
& > img { border-width: 2px; }
}
}
img { object-fit: cover; }
img {
object-fit: cover;
&.disabled {
filter: grayscale(100%);
}
}
.icon,
.ava-text::after {
position: absolute;
@@ -242,6 +264,12 @@
width: 4.5rem; // 72 - 63
.ava-text { font-size: 2.75rem; }
&.modern {
width: 5rem;
.ava-text { font-size: 3rem; }
}
}
.hulyAvatarSize-x-large {
width: 7.5rem; // 120 - 105
@@ -297,7 +325,14 @@
.small-font &.small { width: 9px; }
&.small { width: 10px; }
.small-font &.medium { width: 11px; }
&.medium { width: 13px; }
&.medium {
width: 13px;
&.modern {
right: 4px;
bottom: 4px;
width: 14px;
}
}
&.large {
right: -.125rem;
bottom: -.125rem;
@@ -309,7 +344,9 @@
width: 37%;
}
}
&.online { background-color: var(--global-online-color); }
&.online {
background-color: var(--global-online-color);
}
&.offline {
border: 1px solid var(--global-offline-color);
@@ -20,7 +20,7 @@
import { Card } from '@hcengineering/card'
import { getCurrentAccount } from '@hcengineering/core'
import { EmojiPopup, getEventPositionElement, showPopup, Action as MenuAction } from '@hcengineering/ui'
import { personByPersonIdStore } from '@hcengineering/contact-resources'
import { PersonPreviewProvider, personByPersonIdStore } from '@hcengineering/contact-resources'
import type { SocialID } from '@hcengineering/communication-types'
import { AttachmentPreview } from '@hcengineering/attachment-resources'
import { Message, MessageType } from '@hcengineering/communication-types'
@@ -136,13 +136,17 @@
<div class="message__body">
<!--TODO: remove on:contextmenu-->
<div class="message__avatar" on:contextmenu={handleContextMenu}>
<Avatar name={author?.name} avatar={author} size={AvatarSize.Small} />
<PersonPreviewProvider value={author}>
<Avatar name={author?.name} avatar={author} size={AvatarSize.Small} />
</PersonPreviewProvider>
</div>
<div class="message__content">
<div class="message__header">
<div class="message__username">
{formatName(author?.name ?? '')}
</div>
<PersonPreviewProvider value={author}>
<div class="message__username">
{formatName(author?.name ?? '')}
</div>
</PersonPreviewProvider>
<div class="message__date">
{formatDate(message.created)}
</div>
@@ -30,11 +30,12 @@
export let element: HTMLButtonElement | undefined = undefined
export let id: string | undefined = undefined
export let dataId: string | undefined = undefined
export let type: 'type-button' | 'type-button-icon' = 'type-button'
</script>
<ButtonBase
bind:element
type={'type-button'}
{type}
{title}
{shape}
{label}
@@ -318,7 +318,7 @@
/>
{#if $tooltip.component && $tooltip.kind !== 'submenu'}
<div
class="popup-tooltip {options.classList}"
class="popup-tooltip {options.classList} {$tooltip.style}"
class:shown
class:doublePadding={$tooltip.label}
use:resizeObserver={(element) => {
@@ -369,12 +369,14 @@
/>
</clipPath>
</svg>
<div
bind:this={nubHTML}
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
class="nub {nubDirection ?? ''}"
class:shown
/>
{#if !$tooltip.noArrow}
<div
bind:this={nubHTML}
style:z-index={($modals.findIndex((t) => t.type === 'tooltip') ?? 1) + 10000}
class="nub {nubDirection ?? ''}"
class:shown
/>
{/if}
{:else if $tooltip.label && $tooltip.kind !== 'submenu'}
<div
class="tooltip {dir ?? ''} {options.classList}"
@@ -470,6 +472,22 @@
&.doublePadding {
padding: 1rem;
}
&.modern {
padding: 0;
border: none;
outline: none;
border-radius: 1.5rem;
box-shadow:
0 6.25rem 5rem rgba(0, 0, 0, 0.15),
0 2.5rem 2rem rgba(0, 0, 0, 0.12),
0 1.5rem 1rem rgba(0, 0, 0, 0.1),
0 0.75rem 0.75rem rgba(0, 0, 0, 0.1),
0 0.375rem 0.375rem rgba(0, 0, 0, 0.08),
0 0.125rem 0.125rem rgba(0, 0, 0, 0.05);
}
&.disabled {
background-color: var(--popup-color-disabled);
}
}
.nub {
+20 -10
View File
@@ -12,7 +12,9 @@ const emptyTooltip: LabelAndProps = {
anchor: undefined,
onUpdate: undefined,
keys: undefined,
kind: 'tooltip'
kind: 'tooltip',
style: undefined,
noArrow: false
}
let storedValue: LabelAndProps = emptyTooltip
export const tooltipstore = derived(modalStore, (modals) => {
@@ -25,11 +27,9 @@ export const tooltipstore = derived(modalStore, (modals) => {
let toHandler: any
export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
if (options === undefined) {
return {}
}
if (options.label === undefined && options.component === undefined) {
if (options?.label === undefined && options?.component === undefined) {
// No tooltip
// TODO: Fix reactive options update in this case
return {}
}
let opt = options
@@ -48,7 +48,9 @@ export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
opt.anchor,
opt.onUpdate,
opt.kind,
opt.keys
opt.keys,
opt.style,
opt.noArrow
)
}, opt.timeout ?? 10)
} else {
@@ -61,7 +63,9 @@ export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
opt.anchor,
opt.onUpdate,
opt.kind,
opt.keys
opt.keys,
opt.style,
opt.noArrow
)
}
}
@@ -86,7 +90,9 @@ export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
opt.anchor,
opt.onUpdate,
opt.kind,
opt.keys
opt.keys,
opt.style,
opt.noArrow
)
}
},
@@ -111,7 +117,9 @@ export function showTooltip (
anchor?: HTMLElement,
onUpdate?: (result: any) => void,
kind?: 'tooltip' | 'submenu' | 'popup',
keys?: string[]
keys?: string[],
style?: 'default' | 'modern',
noArrow?: boolean
): void {
storedValue = {
label,
@@ -123,7 +131,9 @@ export function showTooltip (
onUpdate,
kind,
keys,
type: 'tooltip'
type: 'tooltip',
style,
noArrow
}
modalStore.update((old) => {
const tooltip = old.find((m) => m?.type === 'tooltip') as LabelAndProps | undefined
+2
View File
@@ -306,6 +306,8 @@ export interface LabelAndProps {
kind?: 'tooltip' | 'submenu' | 'popup'
keys?: string[]
timeout?: number
style?: 'default' | 'modern'
noArrow?: boolean
}
export interface ListItem {
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "assets"
}
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Dosažení",
"EarliestAdopter": "Nejakej první adoptovat",
"Epic": "Epic",
"Legendary": "Legendární"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Erfolge",
"EarliestAdopter": "Erste Adopter",
"Epic": "Epic",
"Legendary": "Legendär"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Achievements",
"EarliestAdopter": "Earliest Adopter",
"Epic": "Epic",
"Legendary": "Legendary"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Logros",
"EarliestAdopter": "Primer Adoptante",
"Epic": "Epic",
"Legendary": "Legendario"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Achèvements",
"EarliestAdopter": "Premier Adoptant",
"Epic": "Epic",
"Legendary": "Légendaire"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Riconoscimenti",
"EarliestAdopter": "Primo Adoptante",
"Epic": "Epic",
"Legendary": "Légendaire"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Conquistas",
"EarliestAdopter": "Primeiro Adoptante",
"Epic": "Epic",
"Legendary": "Legendário"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Достижения",
"EarliestAdopter": "Ранний пользователь",
"Epic": "Эпик",
"Legendary": "Легендарный"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "成就",
"EarliestAdopter": "最早采用者",
"Epic": "史诗",
"Legendary": "传奇"
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "@hcengineering/achievement-assets",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Copyright © Hardcore Engineering Inc.",
"template": "@hcengineering/assets-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"test": "jest --passWithNoTests --silent",
"build:docs": "",
"format": "format src",
"build:watch": "compile",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"@types/node": "~20.11.16",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/achievement": "^0.6.0"
}
}
@@ -0,0 +1,6 @@
import { makeLocalesTest } from '@hcengineering/platform'
it(
'Locales are equale',
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
)
+28
View File
@@ -0,0 +1,28 @@
//
// Copyright © 2025 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 { addStringsLoader, loadMetadata } from '@hcengineering/platform'
import achievement, { achievementId } from '@hcengineering/achievement'
const EarliestAdopter = require('../assets/EarliestAdopter.png') as string // eslint-disable-line
const Epic = require('../assets/Epic.png') as string // eslint-disable-line
const Legendary = require('../assets/Legendary.png') as string // eslint-disable-line
loadMetadata(achievement.image, {
EarliestAdopter,
Epic,
Legendary
})
addStringsLoader(achievementId, async (lang: string) => await import(`../lang/${lang}.json`))
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"types": ["node", "jest"],
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
@@ -0,0 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
}
@@ -0,0 +1,51 @@
{
"name": "@hcengineering/achievement-resources",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Copyright © Hardcore Engineering Inc.",
"license": "EPL-2.0",
"scripts": {
"build": "compile ui",
"format": "format src",
"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"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.2.2",
"@types/node": "~20.11.16",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"typescript": "^5.3.3",
"svelte": "^4.2.19",
"svelte-check": "^3.6.9",
"svelte-preprocess": "^5.1.3",
"svelte-loader": "^3.2.0",
"postcss": "^8.4.20",
"autoprefixer": "^10.4.14",
"tailwindcss": "^3.3.5",
"sass": "^1.53.0",
"eslint-plugin-svelte": "^2.35.1",
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/achievement": "^0.6.0",
"@hcengineering/ui": "^0.6.15",
"svelte": "^4.2.19"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
@@ -0,0 +1,37 @@
<!--
// Copyright © 2025 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 achievement from '@hcengineering/achievement'
import { Label } from '@hcengineering/ui'
</script>
<div class="header-container gap-2 px-4">
<span class="text-md font-normal content-color">
<Label label={achievement.string.Achievements} />
</span>
<div class="divider" />
</div>
<style>
.header-container {
display: flex;
padding: 0 0.75rem;
align-items: center;
}
.divider {
width: 100%;
border-top: 1px solid var(--theme-divider-color);
}
</style>
@@ -0,0 +1,50 @@
<!--
// Copyright © 2025 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 { Doc, Ref } from '@hcengineering/core'
import { Image } from '@hcengineering/ui'
import { getMetadata } from '@hcengineering/platform'
import AchievementsHeader from './AchievementsHeader.svelte'
import { getPersonAchievements } from '../utils'
export let personId: Ref<Doc>
export let withHeader: boolean = true
$: personAchievements = getPersonAchievements(personId)
</script>
{#if personAchievements.length > 0}
{#if withHeader}
<AchievementsHeader />
{/if}
<div class="achievements">
{#each personAchievements as achievement}
<div class="icon">
<Image src={getMetadata(achievement.icon)} width="40px" height="60px" />
</div>
{/each}
</div>
{/if}
<style lang="scss">
.achievements {
display: flex;
flex-wrap: wrap;
padding: 0.625rem 0.5rem 0 0.5rem;
order: 1;
}
</style>
@@ -0,0 +1,25 @@
//
// Copyright © 2025 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 Resources } from '@hcengineering/platform'
import PersonAchievementsPresenter from './components/PersonAchievementsPresenter.svelte'
export const achievementResourceId = 'achievement-resources' as const
export default async (): Promise<Resources> => ({
component: {
PersonAchievementsPresenter
}
})
@@ -0,0 +1,22 @@
// Copyright © 2025 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 { mergeIds } from '@hcengineering/platform'
import achievement, { achievementId } from '@hcengineering/achievement'
import { type AnyComponent } from '@hcengineering/ui'
export default mergeIds(achievementId, achievement, {
component: {
PersonAchievementsPresenter: '' as AnyComponent
}
})
@@ -0,0 +1,48 @@
//
// Copyright © 2025 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 achievement from '@hcengineering/achievement'
import { type IntlString, type Asset } from '@hcengineering/platform'
interface PersonAchievement {
icon: Asset
tooltip: IntlString
}
// TODO: Remove and replace with actual person achievements, it is just for demonstration. Implement achievement service.
const possibleAchievements: PersonAchievement[] = [
{ icon: achievement.image.EarliestAdopter, tooltip: achievement.string.EarliestAdopter },
{ icon: achievement.image.Epic, tooltip: achievement.string.Epic },
{ icon: achievement.image.Legendary, tooltip: achievement.string.Legendary }
]
function hashStringToInt (str: string): number {
if (str === undefined) return 0
let hash = 0
if (str.length === 0) return hash
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = (hash << 5) - hash + char
hash |= 0 // Convert to a 32-bit integer
}
return hash
}
export function getPersonAchievements (personId: string): PersonAchievement[] {
const personHash = hashStringToInt(personId)
return possibleAchievements.filter((_, index) => {
return personHash % (index + 1) === 0
})
}
@@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};
@@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+48
View File
@@ -0,0 +1,48 @@
{
"name": "@hcengineering/achievement",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Copyright © Hardcore Engineering Inc.",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"@hcengineering/ui": "^0.6.15"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright © 2025 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 { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform'
/**
* @public
*/
export const achievementId = 'achievement' as Plugin
export default plugin(achievementId, {
image: {
EarliestAdopter: '' as Asset,
Epic: '' as Asset,
Legendary: '' as Asset
},
string: {
Achievements: '' as IntlString,
EarliestAdopter: '' as IntlString,
Epic: '' as IntlString,
Legendary: '' as IntlString
}
})
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}
@@ -197,7 +197,7 @@
{#if $$slots.icon}
<slot name="icon" />
{:else if person}
<Avatar size="medium" {person} name={person.name} />
<Avatar size="medium" {person} name={person.name} showPreview />
{:else}
<SystemAvatar size="medium" />
{/if}
@@ -116,7 +116,7 @@
]}
selected={isListDisplayMode ? 'table' : 'card'}
on:select={(result) => {
if (result.detail !== undefined) isListDisplayMode = result.detail === 'table' ?? false
if (result.detail !== undefined) isListDisplayMode = result.detail === 'table'
}}
/>
</div>
@@ -20,8 +20,12 @@
import chunter from '../plugin'
import { createDirect } from '../utils'
import { openChannelInSidebar } from '../navigation'
import { Asset } from '@hcengineering/platform'
export let employee: Employee
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
export let icon: Asset | undefined = undefined
export let type: 'type-button' | 'type-button-icon' = 'type-button'
async function openDirect (): Promise<void> {
const dm = await createDirect([employee._id])
@@ -41,9 +45,11 @@
</script>
<ModernButton
label={chunter.string.Message}
icon={view.icon.Bubble}
label={type === 'type-button-icon' ? undefined : chunter.string.Message}
icon={icon ?? view.icon.Bubble}
size="small"
iconSize="small"
{type}
{kind}
on:click={openDirect}
/>
+20
View File
@@ -121,4 +121,24 @@
c5.959,1.695,10.706,6.453,12.388,12.416c0.124,0.44,0.525,0.729,0.962,0.729c0.09,0,0.181-0.012,0.272-0.037
c0.531-0.15,0.841-0.703,0.691-1.234C39.887,14.753,34.613,9.467,27.991,7.582z"/>
</symbol>
<symbol id="contacts" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7,5.7c-1.4,0-2.7,0.5-3.5,1.5c-0.9,0.9-1.3,2.3-1.2,3.7c0.2,2.7,2.3,5.1,4.8,5.1c2.5,0,4.6-2.3,4.8-5.1C25.7,8,23.5,5.7,20.7,5.7z M18.5,8.5C18,9,17.7,9.8,17.7,10.8c0.1,2,1.6,3.3,2.9,3.3c1.3,0,2.8-1.3,2.9-3.3c0.1-1.9-1.1-3.2-2.9-3.2C19.8,7.6,19,7.9,18.5,8.5z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4,24.1c1.1-4.2,5.2-6.3,9.3-6.3c4,0,8.2,2,9.3,6.3c0.2,0.9-0.4,2.1-1.6,2.1H13C11.7,26.3,11.2,25.1,11.4,24.1z M13.3,24.4h14.8c-0.9-3-3.9-4.7-7.4-4.7C17.2,19.7,14.1,21.4,13.3,24.4z" />
<path d="M9.6,16.2c-2.1,0-3.9-1.9-4-4.3c-0.1-1.2,0.3-2.3,1-3.1c0.8-0.8,1.8-1.2,3-1.2c1.2,0,2.2,0.4,3,1.3c0.8,0.8,1.1,1.9,1.1,3.1C13.5,14.3,11.7,16.2,9.6,16.2z M9.6,9.5C9,9.5,8.4,9.7,8,10.1c-0.4,0.4-0.6,1-0.5,1.7c0.1,1.4,1.1,2.5,2.2,2.5s2.1-1.2,2.2-2.5c0-0.7-0.2-1.3-0.6-1.7C10.9,9.7,10.3,9.5,9.6,9.5z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M2,22.5c0.9-3.5,4.3-5.2,7.6-5.2c1.3,0,2.6,0.2,3.8,0.8c0.5,0.2,0.7,0.8,0.5,1.2c-0.2,0.5-0.8,0.7-1.2,0.5c-0.9-0.4-1.9-0.6-3.1-0.6c-2.6,0-4.9,1.2-5.7,3.4H10c0.5,0,0.9,0.4,0.9,0.9s-0.4,0.9-0.9,0.9H3.5C2.4,24.4,1.8,23.3,2,22.5L2,22.5z" />
</symbol>
<symbol id="clock" viewBox="0 0 14 14">
<path d="M7.43758 3.49984C7.43758 3.25821 7.24171 3.06234 7.00008 3.06234C6.75846 3.06234 6.56258 3.25821 6.56258 3.49984V6.99984C6.56258 7.11587 6.60868 7.22715 6.69072 7.3092L8.44072 9.0592C8.61158 9.23005 8.88859 9.23005 9.05944 9.0592C9.2303 8.88834 9.2303 8.61133 9.05944 8.44048L7.43758 6.81862V3.49984Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00008 0.729004C3.5368 0.729004 0.729248 3.53655 0.729248 6.99984C0.729248 10.4631 3.5368 13.2707 7.00008 13.2707C10.4634 13.2707 13.2709 10.4631 13.2709 6.99984C13.2709 3.53655 10.4634 0.729004 7.00008 0.729004ZM1.60425 6.99984C1.60425 4.0198 4.02005 1.604 7.00008 1.604C9.98012 1.604 12.3959 4.0198 12.3959 6.99984C12.3959 9.97987 9.98012 12.3957 7.00008 12.3957C4.02005 12.3957 1.60425 9.97987 1.60425 6.99984Z"/>
</symbol>
<symbol id="chat" viewBox="0 0 20 20">
<path d="M7.50357 9.1665H5.82943V10.8332H7.50357V9.1665Z"/>
<path d="M10.8332 9.1665H9.15904V10.8332H10.8332V9.1665Z"/>
<path d="M14.1628 9.1665H12.4886V10.8332H14.1628V9.1665Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99996 1.0415C5.08386 1.0415 1.04346 4.83623 1.04346 9.5835C1.04346 11.875 1.98622 13.8743 3.47407 15.4235L2.73443 18.1527L2.73401 18.1542C2.5954 18.6709 3.11754 19.1115 3.60158 18.8977L3.60494 18.8963L6.67685 17.5182C7.70558 17.9104 8.82735 18.1255 9.99996 18.1255C14.916 18.1255 18.9565 14.3309 18.9565 9.5835C18.9565 4.83623 14.916 1.0415 9.99996 1.0415ZM2.29346 9.5835C2.29346 5.58593 5.71334 2.2915 9.99996 2.2915C14.2866 2.2915 17.7065 5.58593 17.7065 9.5835C17.7065 13.5812 14.2866 16.8755 9.99996 16.8755C8.89636 16.8755 7.84897 16.6564 6.9024 16.2626L6.65299 16.1589L4.28155 17.2227L4.87167 15.0452L4.60601 14.7886C3.17406 13.4056 2.29346 11.6207 2.29346 9.5835Z"/>
</symbol>
<symbol id="user" viewBox="0 0 20 20">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 1.0415C7.58366 1.0415 5.62491 3.00026 5.62491 5.4165C5.62491 7.83275 7.58366 9.7915 9.99991 9.7915C12.4162 9.7915 14.3749 7.83275 14.3749 5.4165C14.3749 3.00026 12.4162 1.0415 9.99991 1.0415ZM6.87491 5.4165C6.87491 3.69062 8.27402 2.2915 9.99991 2.2915C11.7258 2.2915 13.1249 3.69061 13.1249 5.4165C13.1249 7.1424 11.7258 8.5415 9.99991 8.5415C8.27402 8.5415 6.87491 7.14239 6.87491 5.4165Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16145 12.3642C8.12316 10.6006 11.8766 10.6006 14.8383 12.3642C14.9352 12.4219 15.055 12.4894 15.1905 12.5659C15.7845 12.901 16.6825 13.4076 17.2976 14.0097C17.6823 14.3863 18.0479 14.8825 18.1143 15.4905C18.185 16.137 17.903 16.7437 17.3371 17.2828C16.3609 18.2128 15.1894 18.9582 13.6742 18.9582H6.32563C4.81037 18.9582 3.63889 18.2128 2.66269 17.2829C2.09684 16.7438 1.81477 16.137 1.88545 15.4905C1.9519 14.8825 2.31744 14.3863 2.70217 14.0097C3.31731 13.4076 4.21525 12.901 4.80926 12.5659C4.94481 12.4894 5.06453 12.4219 5.16145 12.3642ZM5.80098 13.4382C8.36863 11.9092 11.6312 11.9092 14.1988 13.4382C14.3387 13.5215 14.492 13.6084 14.6526 13.6995C15.2464 14.0364 15.9398 14.4298 16.4232 14.903C16.7233 15.1967 16.8513 15.4391 16.8717 15.6263C16.888 15.7749 16.8504 16.02 16.4749 16.3778C15.6117 17.2002 14.7345 17.7082 13.6742 17.7082H6.32563C5.26526 17.7082 4.38812 17.2002 3.52491 16.3778C3.14938 16.0201 3.1118 15.7749 3.12804 15.6263C3.14851 15.4391 3.27644 15.1967 3.57654 14.903C4.05995 14.4298 4.75331 14.0365 5.34715 13.6996C5.50773 13.6085 5.66107 13.5215 5.80098 13.4382Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+3 -1
View File
@@ -109,6 +109,8 @@
"ViewProfile": "Zobrazit profil",
"Viber": "Viber",
"ViberPlaceholder": "Viber",
"UserProfile": "Uživatelský profil"
"UserProfile": "Uživatelský profil",
"DeactivatedAccount": "Deaktivovaný účet",
"LocalTime": "místní čas"
}
}
+3 -1
View File
@@ -109,6 +109,8 @@
"ViewProfile": "Profil anzeigen",
"Viber": "Viber",
"ViberPlaceholder": "Viber",
"UserProfile": "Benutzerprofil"
"UserProfile": "Benutzerprofil",
"DeactivatedAccount": "Deaktivierter Account",
"LocalTime": "Ortszeit"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "Social IDs",
"Type": "Type",
"Confirmed": "Confirmed",
"UserProfile": "User profile"
"UserProfile": "User profile",
"DeactivatedAccount": "Deactivated account",
"LocalTime": "local time"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "Identificaciones sociales",
"Type": "Tipo",
"Confirmed": "Confirmado",
"UserProfile": "Perfil de usuario"
"UserProfile": "Perfil de usuario",
"DeactivatedAccount": "Cuenta desactivada",
"LocalTime": "hora local"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "Identifiants sociaux",
"Type": "Type",
"Confirmed": "Confirmé",
"UserProfile": "Profil utilisateur"
"UserProfile": "Profil utilisateur",
"DeactivatedAccount": "Compte désactivé",
"LocalTime": "heure locale"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "ID social",
"Type": "Tipo",
"Confirmed": "Confermato",
"UserProfile": "Profilo utente"
"UserProfile": "Profilo utente",
"DeactivatedAccount": "Account disattivato",
"LocalTime": "ora locale"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "Identificações sociais",
"Type": "Tipo",
"Confirmed": "Confirmado",
"UserProfile": "Perfil do usuário"
"UserProfile": "Perfil do usuário",
"DeactivatedAccount": "Conta desativada",
"LocalTime": "hora local"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "Социальные ID",
"Type": "Тип",
"Confirmed": "Подтвержден",
"UserProfile": "Профиль пользователя"
"UserProfile": "Профиль пользователя",
"DeactivatedAccount": "Деактивированный аккаунт",
"LocalTime": "местного времени"
}
}
+3 -1
View File
@@ -113,6 +113,8 @@
"SocialIds": "社交 ID",
"Type": "类型",
"Confirmed": "已确认",
"UserProfile": "用户资料"
"UserProfile": "用户资料",
"DeactivatedAccount": "已停用账户",
"LocalTime": "当地时间"
}
}
+12 -1
View File
@@ -43,6 +43,17 @@ loadMetadata(contact.icon, {
Profile: `${icons}#profile`,
KickUser: `${icons}#kickUser`,
ComponentMembers: `${icons}#componentMembers`,
Contacts: `${icons}#contacts`
Contacts: `${icons}#contacts`,
Clock: `${icons}#clock`,
Chat: `${icons}#chat`,
User: `${icons}#user`
})
addStringsLoader(contactId, async (lang: string) => await import(`../lang/${lang}.json`))
const ProfileBackground = require('../assets/profile-background.png') as string // eslint-disable-line
const ProfileBackgroundLight = require('../assets/profile-background-light.png') as string // eslint-disable-line
loadMetadata(contact.image, {
ProfileBackground,
ProfileBackgroundLight
})
+1
View File
@@ -62,6 +62,7 @@
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/account-client": "^0.6.0",
"@hcengineering/achievement": "^0.6.0",
"svelte": "^4.2.19",
"crypto-js": "^4.2.0"
}
@@ -1,5 +1,5 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
// Copyright © 2025 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
@@ -32,24 +32,33 @@
getPlatformAvatarColorForTextDef,
getPlatformColor,
IconSize,
themeStore
themeStore,
tooltip
} from '@hcengineering/ui'
import { onMount } from 'svelte'
import { AccountUuid, type Data, PersonUuid, Ref, type WithLookup } from '@hcengineering/core'
import { loadUsersStatus, statusByUserStore } from '../utils'
import AvatarInstance from './AvatarInstance.svelte'
import { getPreviewPopup } from './person/utils'
export let person: (Data<WithLookup<AvatarInfo>> & { _id?: Ref<Person>, personUuid?: PersonUuid }) | undefined =
undefined
export let person:
| (Data<WithLookup<AvatarInfo>> & { _id?: Ref<Person>, personUuid?: PersonUuid })
| Person
| undefined = undefined
export let name: string | null | undefined = undefined
export let direct: Blob | undefined = undefined
export let size: IconSize
export let statusSize: IconSize | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let variant: 'circle' | 'roundedRect' | 'none' = 'roundedRect'
export let borderColor: number | undefined = undefined
export let showStatus: boolean = false
export let adaptiveName: boolean = false
export let showPreview: boolean = false
export let disabled: boolean = false
export let style: 'modern' | undefined = undefined
export let clickable: boolean = false
export function pulse (): void {
avatarInst.pulse()
@@ -112,8 +121,35 @@
person?.personUuid !== undefined && $statusByUserStore.get(person.personUuid as AccountUuid)?.online === true
</script>
{#if showStatus && person}
<div class="relative">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-presenter" use:tooltip={getPreviewPopup(person, showPreview)} on:click>
{#if showStatus && person}
<div class="relative">
<AvatarInstance
bind:this={avatarInst}
{url}
srcset={srcSet}
{displayName}
{size}
{icon}
{variant}
{color}
{bColor}
bind:element
{adaptiveName}
{disabled}
{style}
{clickable}
withStatus
/>
<div
class="hulyAvatar-statusMarker {statusSize ?? size} {style}"
class:online={isOnline}
class:offline={!isOnline}
/>
</div>
{:else}
<AvatarInstance
bind:this={avatarInst}
{url}
@@ -125,23 +161,16 @@
{color}
{bColor}
bind:element
withStatus
{adaptiveName}
{disabled}
{style}
{clickable}
/>
<div class="hulyAvatar-statusMarker {size}" class:online={isOnline} class:offline={!isOnline} />
</div>
{:else}
<AvatarInstance
bind:this={avatarInst}
{url}
srcset={srcSet}
{displayName}
{size}
{icon}
{variant}
{color}
{bColor}
bind:element
{adaptiveName}
/>
{/if}
{/if}
</div>
<style>
.flex-presenter {
cursor: pointer;
}
</style>
@@ -1,5 +1,5 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
// Copyright © 2025 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
@@ -17,6 +17,9 @@
import { themeStore } from '@hcengineering/theme'
import { AnySvelteComponent, ColorDefinition, Icon, IconSize, resizeObserver } from '@hcengineering/ui'
import AvatarIcon from './icons/Avatar.svelte'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let url: string | undefined
export let srcset: string | undefined
@@ -29,6 +32,13 @@
export let withStatus: boolean = false
export let element: HTMLElement
export let adaptiveName: boolean = false
export let disabled: boolean = false
export let style: 'modern' | undefined = undefined
export let clickable: boolean = false
function handleClick (): void {
dispatch('click')
}
export function pulse (): void {
if (element === undefined) return
@@ -50,21 +60,30 @@
}
$: hasImg = url != null && !imgError
$: background =
!hasImg && disabled
? 'var(--theme-popup-deactivated)'
: color && !hasImg
? color.icon
: 'var(--theme-button-default)'
</script>
{#if (size === 'full' || adaptiveName) && !url && displayName && displayName !== ''}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={element}
class="hulyAvatar-container hulyAvatarSize-{size} {variant}"
class="hulyAvatar-container hulyAvatarSize-{size} {variant} {style} {clickable ? 'clickable' : ''}"
class:no-img={!hasImg && color}
class:bordered={!hasImg && color === undefined}
class:border={bColor !== undefined}
class:withStatus
style:--border-color={bColor ?? 'var(--primary-button-default)'}
style:background-color={color && !hasImg ? color.icon : 'var(--theme-button-default)'}
style:background-color={background}
use:resizeObserver={(element) => {
fontSize = element.clientWidth * 0.6
}}
on:click={handleClick}
>
<div
class="ava-text"
@@ -74,22 +93,31 @@
/>
</div>
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={element}
class="hulyAvatar-container hulyAvatarSize-{size} stat {variant}"
class="hulyAvatar-container hulyAvatarSize-{size} {variant} {style} {clickable ? 'clickable' : ''}"
class:no-img={!hasImg && color}
class:bordered={!hasImg && color === undefined}
class:border={bColor !== undefined}
class:withStatus
style:--border-color={bColor ?? 'var(--primary-button-default)'}
style:background-color={color && !hasImg ? color.icon : 'var(--theme-button-default)'}
style:background-color={background}
on:click={handleClick}
>
{#if url && !imgError}
<img class="hulyAvatarSize-{size} ava-image" src={url} {srcset} alt={''} on:error={handleImgError} />
<img
class="hulyAvatarSize-{size} ava-image {disabled ? 'disabled' : ''}"
src={url}
{srcset}
alt={''}
on:error={handleImgError}
/>
{:else if displayName && displayName !== ''}
<div
class="ava-text"
style:color={color ? color.iconText : 'var(--primary-button-color)'}
style:color={disabled ? 'white' : color ? color.iconText : 'var(--primary-button-color)'}
data-name={displayName.toLocaleUpperCase()}
/>
{:else}
@@ -99,3 +127,17 @@
{/if}
</div>
{/if}
<style>
.clickable {
cursor: pointer;
}
.clickable > * {
pointer-events: none;
}
.clickable img {
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
</style>
@@ -93,18 +93,16 @@
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cursor-pointer" on:click|self={showSelectionPopup}>
<AvatarComponent
{direct}
{size}
{icon}
person={{
avatarType: selectedAvatarType,
avatarProps: selectedAvatarProps,
avatar: selectedAvatar
}}
{name}
/>
</div>
<AvatarComponent
{direct}
{size}
{icon}
person={{
avatarType: selectedAvatarType,
avatarProps: selectedAvatarProps,
avatar: selectedAvatar
}}
{name}
clickable
on:click={showSelectionPopup}
/>
@@ -1,13 +1,13 @@
<script lang="ts">
import { Employee, Person } from '@hcengineering/contact'
import { Person } from '@hcengineering/contact'
import { Ref, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import ui, { IconSize, LabelAndProps } from '@hcengineering/ui'
import { PersonLabelTooltip, employeeByIdStore, personByIdStore } from '..'
import ui, { IconSize } from '@hcengineering/ui'
import { PersonLabelTooltip, personByIdStore } from '..'
import PersonPresenter from '../components/PersonPresenter.svelte'
import contact from '../plugin'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
import { getPreviewPopup } from './person/utils'
export let value: Ref<Person> | WithLookup<Person> | null | undefined
export let showPopup: boolean = true
@@ -35,24 +35,13 @@
$: employeeValue = person != null ? h.as(person, contact.mixin.Employee) : undefined
$: active = employeeValue?.active ?? false
function getPreviewPopup (active: boolean, value: Employee | undefined): LabelAndProps | undefined {
if (!active || value === undefined || !showPopup) {
return undefined
}
return {
component: EmployeePreviewPopup,
props: { employeeId: value._id },
timeout: 300
}
}
</script>
<PersonPresenter
value={employeeValue}
{tooltipLabels}
onEdit={onEmployeeEdit}
customTooltip={getPreviewPopup(active, employeeValue)}
customTooltip={getPreviewPopup(employeeValue, showPopup)}
{shouldShowAvatar}
{shouldShowName}
{avatarSize}
@@ -1,149 +0,0 @@
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { ModernButton, navigate, resizeObserver } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import view from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import contact from '../plugin'
import Avatar from './Avatar.svelte'
import { employeeByIdStore, statusByUserStore } from '../utils'
import { EmployeePresenter } from '../index'
export let employeeId: Ref<Employee>
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let employee: Employee | undefined = undefined
$: employee = $employeeByIdStore.get(employeeId)
$: isOnline = employee?.personUuid !== undefined && $statusByUserStore.get(employee.personUuid)?.online === true
// const statusesQuery = createQuery()
// let editable = false
// let status: Status | undefined = undefined
// $: editable = employeeId === me
// statusesQuery.query(contact.class.Status, { attachedTo: employeeId }, (res) => {
// status = res[0]
// })
// function setStatus (): void {
// if (!employee) return
// showPopup(
// EmployeeSetStatusPopup,
// {
// currentStatus: status
// },
// undefined,
// () => {},
// async (newStatus: Status) => {
// if (status && newStatus) {
// await client.updateDoc(contact.class.Status, status.space, status._id, { ...newStatus })
// } else if (status && !newStatus) {
// await client.removeDoc(contact.class.Status, status.space, status._id)
// } else {
// await client.addCollection(contact.class.Status, employee.space, employeeId, contact.mixin.Employee, 'statuses', {
// name: newStatus.name,
// dueDate: newStatus.dueDate
// })
// }
// }
// )
// dispatch('close')
// }
async function viewProfile (): Promise<void> {
if (employee === undefined) return
const panelComponent = hierarchy.classHierarchyMixin(employee._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
const comp = panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, employee, {}, comp)
navigate(loc)
}
</script>
<div
class="root flex-col"
use:resizeObserver={() => {
dispatch('changeContent')
}}
>
{#if employee}
<div class="flex-presenter flex-gap-2 p-2">
<Avatar size="large" person={employee} name={employee.name} />
<span class="username">
<EmployeePresenter value={employee} shouldShowAvatar={false} showPopup={false} compact />
</span>
<span class="hulyAvatar-statusMarker small relative mt-0-5" class:online={isOnline} class:offline={!isOnline} />
</div>
<div class="separator" />
<div class="flex-presenter flex-gap-2 p-2">
<ComponentExtensions extension={contact.extension.EmployeePopupActions} props={{ employee }} />
<ModernButton
label={contact.string.ViewProfile}
icon={contact.icon.Person}
size="small"
iconSize="small"
on:click={viewProfile}
/>
</div>
<!--{#if status}-->
<!-- <div class="pb-2">-->
<!-- <Label label={contact.string.Status} />-->
<!-- <div class="flex-row-stretch statusContainer">-->
<!-- <div class="pr-2">-->
<!-- <EmployeeStatusPresenter {employee} withTooltip={false} />-->
<!-- </div>-->
<!-- {#if editable}-->
<!-- <div class="setStatusButton">-->
<!-- <Button icon={Edit} title={contact.string.SetStatus} on:click={setStatus} />-->
<!-- </div>-->
<!-- {/if}-->
<!-- </div>-->
<!-- </div>-->
<!--{:else if editable}-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- &lt;!&ndash; svelte-ignore a11y-no-static-element-interactions &ndash;&gt;-->
<!-- <div class="flex-row-stretch over-underline pb-2" on:click={setStatus}>-->
<!-- <Label label={contact.string.SetStatus} />-->
<!-- </div>-->
<!--{/if}-->
{/if}
</div>
<style lang="scss">
.root {
display: flex;
flex-direction: column;
width: auto;
min-height: 0;
min-width: 0;
max-width: 30rem;
background: var(--theme-popup-color);
user-select: none;
}
.separator {
height: 1px;
width: 100%;
background: var(--global-ui-BorderColor);
}
.username {
font-weight: 500;
}
//.statusContainer {
// .setStatusButton {
// opacity: 0;
// }
//
// &:hover .setStatusButton {
// opacity: 1;
// }
//}
</style>
@@ -209,17 +209,7 @@
<div class="flex-col-center gapV-4 mx-6">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="cursor-pointer"
on:click|self={(e) => {
if (imageOnly) {
handleImageAvatarClick()
} else {
if (selectedAvatarType === AvatarType.IMAGE) handleImageAvatarClick()
else if (selectedAvatarType === AvatarType.COLOR) showColorPopup(e)
}
}}
>
<div class="cursor-pointer">
<AvatarComponent
person={{
avatarType: selectedAvatarType,
@@ -230,6 +220,15 @@
size={'2x-large'}
{icon}
{name}
clickable
on:click={(e) => {
if (imageOnly) {
void handleImageAvatarClick()
} else {
if (selectedAvatarType === AvatarType.IMAGE) void handleImageAvatarClick()
else if (selectedAvatarType === AvatarType.COLOR) showColorPopup(e)
}
}}
/>
</div>
<TabList
@@ -0,0 +1,74 @@
<!--
// Copyright © 2025 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 { Label } from '@hcengineering/ui'
import contact from '@hcengineering/contact'
import type { IntlString } from '@hcengineering/platform'
export let header: IntlString = contact.string.DeactivatedAccount
</script>
<div class="deactivated-header">
<div class="deactivated-label">
<div class="deactivated-text">
<span class="text-normal font-medium content-color">
<Label label={header} />
</span>
</div>
</div>
<div class="action-buttons">
<slot name="actions" />
</div>
</div>
<style>
.deactivated-text {
justify-content: center;
display: flex;
flex-direction: column;
}
.deactivated-label {
flex: 1 1 0;
height: 2rem;
border-radius: 0.375rem;
justify-content: flex-start;
align-items: center;
display: flex;
}
.action-buttons {
justify-content: flex-start;
align-items: center;
gap: 0.75rem;
display: flex;
}
.deactivated-header {
width: 22.5rem;
align-self: stretch;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
background: var(--theme-popup-deactivated);
justify-content: flex-start;
align-items: center;
gap: 0.75rem;
display: inline-flex;
border: none;
outline: none;
}
</style>
@@ -0,0 +1,158 @@
<!--
// Copyright © 2025 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 { Employee } from '@hcengineering/contact'
import { AccountUuid, Class, Doc, Ref } from '@hcengineering/core'
import { ButtonIcon, Loading, navigate } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import ModernProfilePopup from './ModernProfilePopup.svelte'
import contact from '../../plugin'
import Avatar from '../Avatar.svelte'
import { employeeByIdStore, getAccountClient } from '../../utils'
import { EmployeePresenter } from '../../index'
import TimePresenter from './TimePresenter.svelte'
import DeactivatedHeader from './DeactivatedHeader.svelte'
export let _id: Ref<Employee>
export let disabled: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
let employee: Employee | undefined = undefined
let timezone: string | undefined = undefined
let isTimezoneLoading = true
$: employee = $employeeByIdStore.get(_id)
$: void loadPersonTimezone(employee?.personUuid)
async function viewProfile (): Promise<void> {
if (employee === undefined) return
const panelComponent = hierarchy.classHierarchyMixin(employee._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
const comp = panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, employee, {}, comp)
navigate(loc)
}
async function loadPersonTimezone (personId: AccountUuid | undefined): Promise<void> {
if (personId === undefined) return
isTimezoneLoading = true
try {
const accountInfo = await getAccountClient().getAccountInfo(personId)
timezone = accountInfo.timezone
} catch (error) {
console.error(error)
}
isTimezoneLoading = false
}
</script>
<ModernProfilePopup {disabled}>
<div slot="header">
{#if disabled}
<div class="flex-presenter">
<DeactivatedHeader>
<div slot="actions">
<div class="flex-presenter flex-gap-2 flex-center">
<ComponentExtensions
extension={contact.extension.EmployeePopupActions}
props={{ employee, icon: contact.icon.Chat, type: 'type-button-icon' }}
/>
<ButtonIcon icon={contact.icon.User} size="small" iconSize="small" on:click={viewProfile} />
</div>
</div>
</DeactivatedHeader>
</div>
{/if}
</div>
<div slot="content">
{#if !disabled}
<div class="flex-presenter cursor-default flex-gap-2 p-2">
<Avatar
size="large"
person={employee}
name={employee?.name}
{disabled}
showStatus
statusSize="medium"
style="modern"
clickable
on:click={viewProfile}
/>
<div class="flex-col flex-gap-0-5">
<EmployeePresenter value={employee} shouldShowAvatar={false} showPopup={false} compact accent />
<span class="flex-presenter cursor-default">
{#if isTimezoneLoading}
<Loading size="small" />
{:else if timezone != null}
<TimePresenter {timezone} />
{/if}
</span>
</div>
</div>
<div class="py-1">
<ComponentExtensions
extension={contact.extension.PersonAchievementsPresenter}
props={{
personId: _id
}}
/>
</div>
{:else}
<div class="flex-presenter flex-gap-2 p-2">
<div class="flex-presenter">
<Avatar
size="large"
person={employee}
name={employee?.name}
{disabled}
style="modern"
clickable
on:click={viewProfile}
/>
</div>
<div class="flex-col">
<EmployeePresenter value={employee} shouldShowAvatar={false} showPopup={false} compact accent />
</div>
</div>
{/if}
</div>
<div slot="actions">
{#if !disabled}
<div class="flex-presenter flex-gap-2 flex-center">
<div class="button-container">
<ComponentExtensions
extension={contact.extension.EmployeePopupActions}
props={{ employee, icon: contact.icon.Chat, type: 'type-button-icon' }}
/>
</div>
<div class="button-container">
<ButtonIcon icon={contact.icon.User} size="small" iconSize="small" on:click={viewProfile} />
</div>
</div>
{/if}
</div>
</ModernProfilePopup>
<style lang="scss">
.button-container {
border-radius: var(--small-BorderRadius);
display: flex;
background-color: var(--theme-button-container-color);
}
</style>
@@ -0,0 +1,127 @@
<!--
// Copyright © 2025 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 { getMetadata } from '@hcengineering/platform'
import contact from '@hcengineering/contact'
import { getCurrentTheme, isThemeDark } from '@hcengineering/theme'
export let disabled: boolean = false
const backgroundImage = isThemeDark(getCurrentTheme())
? contact.image.ProfileBackground
: contact.image.ProfileBackgroundLight
$: style = disabled ? 'gray' : ''
</script>
<div class="user-popup {style}">
<slot name="header" />
{#if !disabled}
<div class="image-container">
<div
class="img {style}"
style={`background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 25%, var(--theme-popup-color) 95%), url("${getMetadata(backgroundImage)}"); background-size: cover;`}
/>
</div>
{/if}
<div class="action-container">
<slot name="actions" />
</div>
<div class="content">
<slot name="content" />
</div>
</div>
<style lang="scss">
.user-popup {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0;
width: 22.5rem;
}
.content,
.content {
padding-left: 0.25rem;
isolation: isolate;
order: 0;
width: 100%;
.main-container {
padding: 1rem 0;
gap: 1rem;
}
.button {
position: absolute;
width: 1.75rem;
height: 1.75rem;
right: 1rem;
top: 1rem;
border-radius: 0.375rem;
display: flex;
justify-content: center;
align-items: center;
}
}
.image-container {
/* image-container */
width: 100%;
min-height: 9rem;
display: flex;
/* Inside auto layout */
flex: none;
order: 0;
align-self: stretch;
flex-grow: 0;
margin: -1.875rem 0;
z-index: -100;
}
.action-container {
position: absolute;
top: 0;
right: 0;
padding: 1rem;
display: flex;
align-items: flex-start;
flex: none;
align-self: stretch;
flex-grow: 0;
}
.img {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 9rem;
border-radius: 0;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -100;
&.gray {
filter: gray;
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
}
</style>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Person } from '@hcengineering/contact'
import { Ref, WithLookup } from '@hcengineering/core'
import { tooltip } from '@hcengineering/ui'
import { personByIdStore } from '../..'
import { getPreviewPopup } from './utils'
export let value: Ref<Person> | WithLookup<Person> | null | undefined
export let showPopup: boolean = true
$: person = typeof value === 'string' ? ($personByIdStore.get(value) as Person) : (value as Person)
</script>
<div class="flex" use:tooltip={getPreviewPopup(person, showPopup)}>
<slot />
</div>
@@ -0,0 +1,56 @@
<!--
// Copyright © 2025 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 { Icon, Label } from '@hcengineering/ui'
import contact from '@hcengineering/contact'
export let timezone: string
function displayTimeInTimezone (timezone: string): string {
const options: Intl.DateTimeFormatOptions = {
timeZone: timezone,
hour: '2-digit',
minute: '2-digit'
}
const formatter = new Intl.DateTimeFormat([], options)
const now = new Date()
return formatter.format(now)
}
</script>
<div class="time-container">
<div class="clock-icon">
<Icon icon={contact.icon.Clock} size={'x-small'} />
</div>
<div class="text-normal font-normal content-color select-text">
<span class="time-text-span">{displayTimeInTimezone(timezone)} <Label label={contact.string.LocalTime} /></span>
</div>
</div>
<style>
.clock-icon {
position: relative;
color: var(--theme-content-color);
}
.time-container {
justify-content: flex-start;
align-items: center;
gap: 0.25rem;
display: inline-flex;
}
</style>
@@ -0,0 +1,37 @@
// Copyright © 2025 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 { getClient } from '@hcengineering/presentation'
import type { LabelAndProps } from '@hcengineering/ui'
import contact from '../../plugin'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
import type { Class, Ref } from '@hcengineering/core'
const client = getClient()
const h = client.getHierarchy()
export function getPreviewPopup (
person: Person | { _id?: Ref<Person>, _class?: Ref<Class<Person>> } | undefined,
showPopup: boolean
): LabelAndProps | undefined {
if (person?._id === undefined || person?._class === undefined || !showPopup) return
const isPerson = person != null && h.isDerived(person._class, contact.class.Person)
const employee = isPerson ? h.as(person as Person, contact.mixin.Employee) : undefined
return {
component: EmployeePreviewPopup,
props: { _id: person._id, classList: ['modern'], disabled: employee?.active === false },
timeout: 300,
style: 'modern',
noArrow: true
}
}
+3 -1
View File
@@ -124,6 +124,7 @@ import IconAddMember from './components/icons/AddMember.svelte'
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
import IconMembers from './components/icons/Members.svelte'
import ContactNamePresenter from './components/ContactNamePresenter.svelte'
import PersonPreviewProvider from './components/person/PersonPreviewProvider.svelte'
import { get } from 'svelte/store'
import { canResendInvitation } from './visibilityTester'
@@ -202,7 +203,8 @@ export {
UserInfo,
UsersList,
UsersPopup,
PersonIdFilter
PersonIdFilter,
PersonPreviewProvider
}
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
+3 -8
View File
@@ -61,7 +61,7 @@ import core, {
} from '@hcengineering/core'
import login from '@hcengineering/login'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { getEmbeddedLabel, getMetadata, getResource, type IntlString, translate } from '@hcengineering/platform'
import { getMetadata, getResource, type IntlString, translate } from '@hcengineering/platform'
import presentation, { createQuery, getClient, onClient } from '@hcengineering/presentation'
import { type TemplateDataProvider } from '@hcengineering/templates'
import {
@@ -78,6 +78,7 @@ import { type LocationData } from '@hcengineering/workbench'
import { derived, get, type Readable, writable } from 'svelte/store'
import contact from './plugin'
import { getPreviewPopup } from './components/person/utils'
export function formatDate (dueDateMs: Timestamp): string {
return new Date(dueDateMs).toLocaleString('default', {
@@ -545,13 +546,7 @@ export async function contactTitleProvider (client: Client, ref: Ref<Contact>, d
}
export function getPersonTooltip (client: Client, value: Person | null | undefined): LabelAndProps | undefined {
const hierarchy = client.getHierarchy()
return value == null
? undefined
: {
label: getEmbeddedLabel(getName(hierarchy, value))
}
return value == null ? undefined : getPreviewPopup(value, true)
}
export async function channelIdentifierProvider (client: Client, ref: Ref<Channel>, doc?: Channel): Promise<string> {
+13 -3
View File
@@ -292,7 +292,14 @@ export const contactPlugin = plugin(contactId, {
Profile: '' as Asset,
KickUser: '' as Asset,
Contacts: '' as Asset,
Viber: '' as Asset
Viber: '' as Asset,
Clock: '' as Asset,
Chat: '' as Asset,
User: '' as Asset
},
image: {
ProfileBackground: '' as Asset,
ProfileBackgroundLight: '' as Asset
},
space: {
Contacts: '' as Ref<Space>
@@ -327,7 +334,9 @@ export const contactPlugin = plugin(contactId, {
SocialIds: '' as IntlString,
Type: '' as IntlString,
Confirmed: '' as IntlString,
UserProfile: '' as IntlString
UserProfile: '' as IntlString,
DeactivatedAccount: '' as IntlString,
LocalTime: '' as IntlString
},
viewlet: {
TableMember: '' as Ref<Viewlet>,
@@ -361,7 +370,8 @@ export const contactPlugin = plugin(contactId, {
MentionCommonNotificationType: '' as Ref<Doc>
},
extension: {
EmployeePopupActions: '' as ComponentExtensionId
EmployeePopupActions: '' as ComponentExtensionId,
PersonAchievementsPresenter: '' as ComponentExtensionId
},
store: {
Permissions: '' as Resource<Readable<PermissionsStore>>
+2 -1
View File
@@ -132,6 +132,7 @@
"RemoveRelationConfirmation": "Chcete odstranit vztah?",
"MasterDetail": "Hlavní-detailní",
"Tree": "Strom",
"Document": "Dokument"
"Document": "Dokument",
"Loading": "Načítání..."
}
}
+2 -1
View File
@@ -132,6 +132,7 @@
"RemoveRelationConfirmation": "Möchten Sie die Beziehung entfernen?",
"MasterDetail": "Master-detail",
"Tree": "Baum",
"Document": "Dokument"
"Document": "Dokument",
"Loading": "Laden..."
}
}
+2 -1
View File
@@ -132,6 +132,7 @@
"RemoveRelationConfirmation": "Do you want to remove relation?",
"MasterDetail": "Master-detail",
"Tree": "Tree",
"Document": "Document"
"Document": "Document",
"Loading": "Loading..."
}
}
+2 -1
View File
@@ -127,6 +127,7 @@
"RemoveRelationConfirmation": "¿Quieres eliminar la relación?",
"MasterDetail": "Maestro-detalle",
"Tree": "Árbol",
"Document": "Documento"
"Document": "Documento",
"Loading": "Cargando..."
}
}
+2 -1
View File
@@ -127,6 +127,7 @@
"RemoveRelationConfirmation": "Voulez-vous supprimer la relation ?",
"MasterDetail": "Maître-détail",
"Tree": "Arbre",
"Document": "Document"
"Document": "Document",
"Loading": "Chargement..."
}
}

Some files were not shown because too many files have changed in this diff Show More