Support for rating system (#10124)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-10-24 10:10:01 +07:00
committed by GitHub
parent 768ff39ab1
commit ffd8d90378
137 changed files with 5005 additions and 712 deletions
+39 -16
View File
@@ -97,7 +97,7 @@
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
// "SERVER_PROVIDER":"uweb"
"SERVER_PROVIDER": "ws",
"MODEL_VERSION": "0.7.230",
"MODEL_VERSION": "0.7.278",
// "version": "0.7.0",
"COMMUNICATION_API_ENABLED": "true",
"ELASTIC_INDEX_NAME": "local_storage_index",
@@ -408,27 +408,23 @@
"protocol": "inspector"
},
{
"name": "Debug backup tool",
"name": "Debug tool",
"type": "node",
"request": "launch",
"args": [
"src/__start.ts",
"backup-restore",
"/Users/haiodo/Develop/private/platform/tests/sanity-ws",
"sanity-ws",
"--upgrade"
],
"args": ["src/__start.ts", "calculate-ratings", "2d790fe8-faba-4c2e-afb1-5610445966bb"],
"env": {
"MODEL_VERSION": "0.7.153",
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"ACCOUNTS_URL": "http://localhost:3003",
"TRANSACTOR_URL": "ws://localhost:3334",
"ACCOUNT_DB_URL": "postgresql://root@localhost:26258/defaultdb?sslmode=disable",
"MONGO_URL": "mongodb://localhost:27018",
"ELASTIC_URL": "http://localhost:9201",
"ACCOUNTS_URL": "http://huly.local:3000",
"TRANSACTOR_URL": "ws://localhost:3332",
"ACCOUNT_DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable",
"MONGO_URL": "mongodb://localhost:27017",
"ELASTIC_URL": "http://localhost:9200",
"SERVER_SECRET": "secret",
"DB_URL": "postgresql://root@localhost:26258/defaultdb?sslmode=disable",
"QUEUE_CONFIG": "localhost:19093;-staging"
"DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable",
"QUEUE_CONFIG": "huly.local:19092",
"REGION": "cockroach",
"QUEUE_REGION": "cockroach"
},
"smartStep": false,
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
@@ -625,6 +621,33 @@
"protocol": "inspector",
"outputCapture": "std"
},
{
"name": "Debug Rating service",
"type": "node",
"request": "launch",
"args": ["src/__start__.ts"],
"env": {
"SERVER_SECRET": "secret",
"MODEL_VERSION": "0.7.278",
"STORAGE_CONFIG": "datalake|http://huly.local:4030",
"ACCOUNTS_URL": "http://localhost:3000",
"COLLABORATOR_URL": "ws://huly.local:3078",
"PLATFORM_OPERATION_LOGGING": "true",
"STATS_URL": "http://huly.local:4900",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://huly.local:4318/v1/traces",
"QUEUE_CONFIG": "huly.local:19092",
"MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json",
"REGION": "cockroach",
"QUEUE_REGION": "cockroach",
"DB_URL": "postgresql://root@localhost:26257/defaultdb?sslmode=disable"
},
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"nodeVersionHint": 22,
"cwd": "${workspaceRoot}/services/rating",
"protocol": "inspector",
"outputCapture": "std"
},
{
"name": "Debug qms workers",
"type": "node",
+9 -1
View File
@@ -253,7 +253,7 @@
"summary": "Build docker with platform",
"description": "use to build all docker containers required for platform",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/docker-server.sh && rush docker:up"
"shellCommand": "./common/scripts/docker-server.sh && rush docker:up:server"
},
{
"commandKind": "global",
@@ -263,6 +263,14 @@
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev && docker compose up -d --force-recreate"
},
{
"commandKind": "global",
"name": "docker:up:server",
"summary": "Up development build",
"description": "Up development build",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev && docker compose up -d --force-recreate transactor_cockroach"
},
{
"commandKind": "global",
"name": "docker:local",
+739 -453
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,3 +1,3 @@
rush docker:build -p 20 \
--to @hcengineering/pod-server \
--to @hcengineering/tool
--to @hcengineering/tool || true
+2 -1
View File
@@ -25,4 +25,5 @@ rush docker:build -p 20 \
--to @hcengineering/pod-backup \
--to @hcengineering/backup-api-pod \
--to @hcengineering/pod-billing \
--to @hcengineering/pod-process
--to @hcengineering/pod-process \
--to @hcengineering/pod-rating
+3
View File
@@ -269,6 +269,9 @@
"@hcengineering/inbox": "^0.7.0",
"@hcengineering/inbox-assets": "^0.7.0",
"@hcengineering/inbox-resources": "^0.7.0",
"@hcengineering/rating": "^0.7.0",
"@hcengineering/rating-assets": "^0.7.0",
"@hcengineering/rating-resources": "^0.7.0",
"electron-squirrel-startup": "~1.0.0",
"dotenv": "~16.0.0",
"electron-context-menu": "^4.0.4",
+4
View File
@@ -82,6 +82,7 @@ import communication, { communicationId } from '@hcengineering/communication'
import { emojiId } from '@hcengineering/emoji'
import { hulyMailId } from '@hcengineering/huly-mail'
import { aiAssistantId } from '@hcengineering/ai-assistant'
import { ratingId } from '@hcengineering/rating'
import billingPlugin, { billingId } from '@hcengineering/billing'
import '@hcengineering/activity-assets'
@@ -142,6 +143,7 @@ import '@hcengineering/communication-assets'
import '@hcengineering/billing-assets'
import '@hcengineering/huly-mail-assets'
import '@hcengineering/ai-assistant-assets'
import '@hcengineering/rating-assets'
import analyticsCollector, { analyticsCollectorId } from '@hcengineering/analytics-collector'
import { coreId } from '@hcengineering/core'
@@ -266,6 +268,7 @@ function configureI18n (): void {
addStringsLoader(billingId, async (lang: string) => await import(`@hcengineering/billing-assets/lang/${lang}.json`))
addStringsLoader(hulyMailId, async (lang: string) => await import(`@hcengineering/huly-mail-assets/lang/${lang}.json`))
addStringsLoader(aiAssistantId, async (lang: string) => await import(`@hcengineering/ai-assistant-assets/lang/${lang}.json`))
addStringsLoader(ratingId, async (lang: string) => await import(`@hcengineering/rating-assets/lang/${lang}.json`))
}
export class PlatformBranding {
@@ -444,6 +447,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise<void
addLocation(billingId, () => import(/* webpackChunkName: "billing" */ '@hcengineering/billing-resources'))
addLocation(hulyMailId, () => import(/* webpackChunkName: "huly-mail" */ '@hcengineering/huly-mail-resources'))
addLocation(aiAssistantId, () => import(/* webpackChunkName: "ai-assistant" */ '@hcengineering/ai-assistant-resources'))
addLocation(ratingId, async () => await import(/* webpackChunkName: "rating" */ '@hcengineering/rating-resources'))
setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
+43 -17
View File
@@ -562,23 +562,23 @@ services:
- QUEUE_CONFIG=${QUEUE_CONFIG}
- QUEUE_REGION=cockroach
restart: unless-stopped
# translate:
# image: hardcoreeng/translate
# extra_hosts:
# - 'huly.local:host-gateway'
# depends_on:
# redpanda:
# condition: service_started
# account:
# condition: service_started
# environment:
# - SECRET=secret
# - ACCOUNTS_URL=http://huly.local:3000
# - HULYLAKE_URL=http://huly.local:8096
# - QUEUE_CONFIG=${QUEUE_CONFIG}
# - QUEUE_REGION=cockroach
# - OPENAI_API_KEY=
# restart: unless-stopped
# translate:
# image: hardcoreeng/translate
# extra_hosts:
# - 'huly.local:host-gateway'
# depends_on:
# redpanda:
# condition: service_started
# account:
# condition: service_started
# environment:
# - SECRET=secret
# - ACCOUNTS_URL=http://huly.local:3000
# - HULYLAKE_URL=http://huly.local:8096
# - QUEUE_CONFIG=${QUEUE_CONFIG}
# - QUEUE_REGION=cockroach
# - OPENAI_API_KEY=
# restart: unless-stopped
backup-cockroach:
image: hardcoreeng/backup
extra_hosts:
@@ -629,6 +629,32 @@ services:
- STORAGE=minio|minio?accessKey=minioadmin&secretKey=minioadmin
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
restart: unless-stopped
rating_cockroach:
image: hardcoreeng/rating
extra_hosts:
- 'huly.local:host-gateway'
links:
- cockroach
- minio
- account
- stats
depends_on:
redpanda:
condition: service_started
environment:
- QUEUE_CONFIG=${QUEUE_CONFIG}
- REGION=cockroach
- SERVER_SECRET=secret
- STATS_URL=http://huly.local:4900
- DB_URL=${DB_CR_URL}
- STORAGE_CONFIG=${STORAGE_CONFIG}
- ACCOUNTS_URL=http://huly.local:3000
- COMMUNICATION_TIME_LOGGING_ENABLED=true
- COMMUNICATION_API_ENABLED=true
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
- HYLYLAKE_URL=http://huly.local:8096
restart: unless-stopped
jaeger:
image: jaegertracing/all-in-one:latest
extra_hosts:
+3
View File
@@ -289,6 +289,9 @@
"@hcengineering/inbox": "^0.7.0",
"@hcengineering/inbox-assets": "^0.7.0",
"@hcengineering/inbox-resources": "^0.7.0",
"@hcengineering/rating": "^0.7.0",
"@hcengineering/rating-assets": "^0.7.0",
"@hcengineering/rating-resources": "^0.7.0",
"readable-stream": "^4.7.0",
"svelte": "^4.2.20"
}
+7
View File
@@ -80,6 +80,7 @@ import { emojiId } from '@hcengineering/emoji'
import billingPlugin, { billingId } from '@hcengineering/billing'
import { hulyMailId } from '@hcengineering/huly-mail'
import { aiAssistantId } from '@hcengineering/ai-assistant'
import { ratingId } from '@hcengineering/rating'
import '@hcengineering/activity-assets'
import '@hcengineering/analytics-collector-assets'
@@ -139,6 +140,7 @@ import '@hcengineering/emoji-assets'
import '@hcengineering/billing-assets'
import '@hcengineering/huly-mail-assets'
import '@hcengineering/ai-assistant-assets'
import '@hcengineering/rating-assets'
import { coreId } from '@hcengineering/core'
import presentation, {
@@ -400,6 +402,7 @@ function configureI18n (): void {
aiAssistantId,
async (lang: string) => await import(`@hcengineering/ai-assistant-assets/lang/${lang}.json`)
)
addStringsLoader(ratingId, async (lang: string) => await import(`@hcengineering/rating-assets/lang/${lang}.json`))
}
export async function configurePlatform () {
@@ -680,6 +683,10 @@ export async function configurePlatform () {
inboxId,
async () => await import(/* webpackChunkName: "inbox" */ '@hcengineering/inbox-resources')
)
addLocation(
ratingId,
async () => await import(/* webpackChunkName: "rating" */ '@hcengineering/rating-resources')
)
setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])
+4 -3
View File
@@ -21,9 +21,9 @@
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool",
"run-local-mongo": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4700 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 ./bundle/bundle.js",
"run-local": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' HULYLAKE_URL=http://huly.local:8096 ACCOUNT_DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
"run-local": "(rush bundle --to @hcengineering/tool >/dev/null || true) && cross-env SERVER_SECRET=secret FULLTEXT_URL=http://localhost:4702 ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3332 STORAGE_CONFIG='datalake|http://huly.local:4030' HULYLAKE_URL=http://huly.local:8096 ACCOUNT_DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable DB_URL=postgresql://root@huly.local:26257/defaultdb?sslmode=disable TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 REGION_INFO='cockroach|CockroachDB' MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) QUEUE_CONFIG='localhost:19092' node --expose-gc --max-old-space-size=18000 $TOOL_OPT ./bundle/bundle.js",
"run-local-brk": "rush bundle --to @hcengineering/tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ACCOUNT_DB_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js",
"run": "rush bundle --to @hcengineering/tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
"run": "(rush bundle --to @hcengineering/tool >/dev/null || true ) && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
"upgrade-mongo": "rushx run-local-mongo upgrade-workspace -- $1",
"upgrade": "rushx run-local upgrade-workspace -- $1",
"upgrade-brk": "rushx run-local-brk upgrade-workspace -- $1",
@@ -183,6 +183,7 @@
"@hcengineering/api-client": "^0.7.5",
"@faker-js/faker": "^8.4.1",
"@hcengineering/hulylake-client": "^0.7.6",
"@hcengineering/communication-types": "^0.7.7"
"@hcengineering/communication-types": "^0.7.7",
"@hcengineering/pod-rating": "^0.7.0"
}
}
+56 -20
View File
@@ -27,16 +27,29 @@ import accountPlugin, {
type AccountDB,
type Workspace
} from '@hcengineering/account'
import {
getMongoAccountDB,
type Account as OldAccount,
type Workspace as OldWorkspace
} from '@hcengineering/account-service'
import { getWorkspaceClient as getHulylakeClient } from '@hcengineering/hulylake-client'
import { setMetadata } from '@hcengineering/platform'
import {
createPostgreeDestroyAdapter,
createPostgresAdapter,
createPostgresTxAdapter,
getDBClient,
shutdownPostgres
} from '@hcengineering/postgres'
import {
backup,
backupDownload,
backupFind,
checkBackupIntegrity,
compactBackup,
createFileBackupStorage,
createStorageBackupStorage,
restore,
backupDownload
restore
} from '@hcengineering/server-backup'
import serverClientPlugin, { getAccountClient, getTransactorEndpoint } from '@hcengineering/server-client'
import {
@@ -51,19 +64,6 @@ import {
} from '@hcengineering/server-pipeline'
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
import { createWorkspace, upgradeWorkspace } from '@hcengineering/workspace-service'
import {
getMongoAccountDB,
type Account as OldAccount,
type Workspace as OldWorkspace
} from '@hcengineering/account-service'
import { getWorkspaceClient as getHulylakeClient } from '@hcengineering/hulylake-client'
import {
getDBClient,
createPostgreeDestroyAdapter,
createPostgresAdapter,
createPostgresTxAdapter,
shutdownPostgres
} from '@hcengineering/postgres'
import { faker } from '@faker-js/faker'
import { getPlatformQueue } from '@hcengineering/kafka'
@@ -71,13 +71,14 @@ import { buildStorageFromConfig, createStorageFromConfig, storageConfigFromEnv }
import { program, type Command } from 'commander'
import { updateField } from './workspace'
import { RatingCalculator, ratingEvents, type QueueRatingMessage } from '@hcengineering/pod-rating'
import {
AccountRole,
isArchivingMode,
isDeletingMode,
MeasureMetricsContext,
metricsToString,
SocialId,
SocialIdType,
systemAccountEmail,
systemAccountUuid,
@@ -85,12 +86,12 @@ import {
type Data,
type Doc,
type PersonId,
type PersonUuid,
type Ref,
type Tx,
type Version,
type WorkspaceDataId,
type WorkspaceUuid,
type PersonUuid
type WorkspaceUuid
} from '@hcengineering/core'
import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model'
import {
@@ -129,14 +130,14 @@ import { performGmailAccountMigrations } from './gmail'
import { getToolToken, getWorkspace, getWorkspaceTransactorEndpoint } from './utils'
import { createRestClient } from '@hcengineering/api-client'
import { type CardID } from '@hcengineering/communication-types'
import { sendTransactorEvent } from '@hcengineering/server-tool'
import { existsSync } from 'fs'
import { mkdir, writeFile } from 'fs/promises'
import { dirname } from 'path'
import { migrateWorkspaceMessages } from './communication'
import { restoreMarkupRefs } from './markup'
import { restoreGithubIntegrations } from './restoreGithub'
import { migrateWorkspaceMessages } from './communication'
import { type CardID } from '@hcengineering/communication-types'
const colorConstants = {
colorRed: '\u001b[31m',
@@ -2899,6 +2900,41 @@ export function devTool (
}, dbUrl)
})
program
.command('calculate-ratings <workspace>')
.description('Perform a rating re-calculation')
.option('-q <queue>', 'Send to queue', false)
.option('-r, --region <region>', 'Region')
.action(async (workspace: string, cmd: { queue: boolean | undefined, region: string | undefined }) => {
await withAccountDatabase(async (db) => {
const { txes, dbUrl } = prepareTools()
const ws = await getWorkspace(db, workspace)
if (ws === null) {
throw new Error(`workspace ${workspace} not found`)
}
if (cmd.queue === true) {
const queue = getPlatformQueue('tool', cmd.region ?? '')
const ratingQueue = queue.getProducer<QueueRatingMessage>(toolCtx, 'rating')
await ratingQueue.send(toolCtx, ws.uuid, [ratingEvents.reindex()])
await queue.shutdown()
} else {
const wsIds = {
uuid: ws.uuid,
dataId: ws.dataId,
url: ws.url
}
const calculator = await RatingCalculator.create(toolCtx, txes, wsIds, dbUrl, async () => '')
await calculator.recalculateAll(toolCtx)
await calculator.close()
}
})
})
extendProgram?.(program)
process.on('unhandledRejection', (reason, promise) => {
+2 -1
View File
@@ -134,6 +134,7 @@
"@hcengineering/model-billing": "^0.7.0",
"@hcengineering/model-huly-mail": "^0.7.0",
"@hcengineering/model-ai-assistant": "^0.7.0",
"@hcengineering/model-inbox": "^0.7.0"
"@hcengineering/model-inbox": "^0.7.0",
"@hcengineering/model-rating": "^0.7.0"
}
}
+41 -26
View File
@@ -17,28 +17,36 @@ import core, { coreId, type Data, type PluginConfiguration, type Ref, type Tx, t
import { Builder } from '@hcengineering/model'
import { activityId, createModel as activityModel } from '@hcengineering/model-activity'
import { aiBotId, createModel as aiBotModel } from '@hcengineering/model-ai-bot'
import { attachmentId, createModel as attachmentModel } from '@hcengineering/model-attachment'
import bitrix, { bitrixId, createModel as bitrixModel } from '@hcengineering/model-bitrix'
import board, { boardId, createModel as boardModel } from '@hcengineering/model-board'
import calendar, { calendarId, createModel as calendarModel } from '@hcengineering/model-calendar'
import card, { cardId, createModel as cardModel } from '@hcengineering/model-card'
import chunter, { chunterId, createModel as chunterModel } from '@hcengineering/model-chunter'
import contact, { contactId, createModel as contactModel } from '@hcengineering/model-contact'
import { createModel as coreModel } from '@hcengineering/model-core'
import { desktopDownloadsId, createModel as desktopDownloadsModel } from '@hcengineering/model-desktop-downloads'
import { desktopPreferencesId, createModel as desktopPreferencesModel } from '@hcengineering/model-desktop-preferences'
import { driveId, createModel as driveModel } from '@hcengineering/model-drive'
import gmail, { gmailId, createModel as gmailModel } from '@hcengineering/model-gmail'
import { guestId, createModel as guestModel } from '@hcengineering/model-guest'
import hr, { hrId, createModel as hrModel } from '@hcengineering/model-hr'
import inventory, { inventoryId, createModel as inventoryModel } from '@hcengineering/model-inventory'
import lead, { leadId, createModel as leadModel } from '@hcengineering/model-lead'
import { mediaId, createModel as mediaModel } from '@hcengineering/model-media'
import notification, { notificationId, createModel as notificationModel } from '@hcengineering/model-notification'
import { preferenceId, createModel as preferenceModel } from '@hcengineering/model-preference'
import presentation, { presentationId, createModel as presentationModel } from '@hcengineering/model-presentation'
import rating, { ratingId, createModel as ratingModel } from '@hcengineering/model-rating'
import { recorderId, createModel as recorderModel } from '@hcengineering/model-recorder'
import recruit, { recruitId, createModel as recruitModel } from '@hcengineering/model-recruit'
import { requestId, createModel as requestModel } from '@hcengineering/model-request'
import { aiBotId, createModel as aiBotModel } from '@hcengineering/model-ai-bot'
import { serverActivityId, createModel as serverActivityModel } from '@hcengineering/model-server-activity'
import { serverAiBotId, createModel as serverAiBotModel } from '@hcengineering/model-server-ai-bot'
import { serverAttachmentId, createModel as serverAttachmentModel } from '@hcengineering/model-server-attachment'
import { serverCardId, createModel as serverCardModel } from '@hcengineering/model-server-card'
import { serverCalendarId, createModel as serverCalendarModel } from '@hcengineering/model-server-calendar'
import { serverCardId, createModel as serverCardModel } from '@hcengineering/model-server-card'
import { serverChunterId, createModel as serverChunterModel } from '@hcengineering/model-server-chunter'
import {
serverCollaborationId,
@@ -62,9 +70,7 @@ import { serverTelegramId, createModel as serverTelegramModel } from '@hcenginee
import { serverTemplatesId, createModel as serverTemplatesModel } from '@hcengineering/model-server-templates'
import { serverTrackerId, createModel as serverTrackerModel } from '@hcengineering/model-server-tracker'
import { serverViewId, createModel as serverViewModel } from '@hcengineering/model-server-view'
import { serverAiBotId, createModel as serverAiBotModel } from '@hcengineering/model-server-ai-bot'
import setting, { settingId, createModel as settingModel } from '@hcengineering/model-setting'
import { driveId, createModel as driveModel } from '@hcengineering/model-drive'
import { supportId, createModel as supportModel } from '@hcengineering/model-support'
import { tagsId, createModel as tagsModel } from '@hcengineering/model-tags'
import { taskId, createModel as taskModel } from '@hcengineering/model-task'
@@ -74,13 +80,8 @@ import { textEditorId, createModel as textEditorModel } from '@hcengineering/mod
import { timeId, createModel as timeModel } from '@hcengineering/model-time'
import tracker, { trackerId, createModel as trackerModel } from '@hcengineering/model-tracker'
import { uploaderId, createModel as uploaderModel } from '@hcengineering/model-uploader'
import { recorderId, createModel as recorderModel } from '@hcengineering/model-recorder'
import { mediaId, createModel as mediaModel } from '@hcengineering/model-media'
import view, { viewId, createModel as viewModel } from '@hcengineering/model-view'
import workbench, { workbenchId, createModel as workbenchModel } from '@hcengineering/model-workbench'
import card, { cardId, createModel as cardModel } from '@hcengineering/model-card'
import { desktopPreferencesId, createModel as desktopPreferencesModel } from '@hcengineering/model-desktop-preferences'
import { desktopDownloadsId, createModel as desktopDownloadsModel } from '@hcengineering/model-desktop-downloads'
import document, { documentId, createModel as documentModel } from '@hcengineering/model-document'
import { serverDocumentId, createModel as serverDocumentModel } from '@hcengineering/model-server-document'
@@ -88,41 +89,41 @@ import { serverDocumentId, createModel as serverDocumentModel } from '@hcenginee
import github, { githubId, createModel as githubModel } from '@hcengineering/model-github'
import { serverGithubId, createModel as serverGithubModel } from '@hcengineering/server-github-model'
import { serverTimeId, createModel as serverTimeModel } from '@hcengineering/model-server-time'
import { analyticsCollectorId, createModel as analyticsCollectorModel } from '@hcengineering/model-analytics-collector'
import { exportId, createModel as exportModel } from '@hcengineering/model-export'
import love, { loveId, createModel as loveModel } from '@hcengineering/model-love'
import { printId, createModel as printModel } from '@hcengineering/model-print'
import { exportId, createModel as exportModel } from '@hcengineering/model-export'
import { analyticsCollectorId, createModel as analyticsCollectorModel } from '@hcengineering/model-analytics-collector'
import { serverLoveId, createModel as serverLoveModel } from '@hcengineering/model-server-love'
import { serverProcessId, createModel as serverProcessModel } from '@hcengineering/model-server-process'
import { serverTimeId, createModel as serverTimeModel } from '@hcengineering/model-server-time'
import { questionsId, createModel as questionsModel } from '@hcengineering/model-questions'
import trainings, { trainingId, createModel as trainingModel } from '@hcengineering/model-training'
import aiAssistant, { aiAssistantId, createModel as aiAssistantModel } from '@hcengineering/model-ai-assistant'
import documents, { documentsId, createModel as documentsModel } from '@hcengineering/model-controlled-documents'
import { hulyMailId, createModel as hulyMailModel } from '@hcengineering/model-huly-mail'
import { mailId, createModel as mailModel } from '@hcengineering/model-mail'
import products, { productsId, createModel as productsModel } from '@hcengineering/model-products'
import { questionsId, createModel as questionsModel } from '@hcengineering/model-questions'
import { serverProductsId, createModel as serverProductsModel } from '@hcengineering/model-server-products'
import { serverTrainingId, createModel as serverTrainingModel } from '@hcengineering/model-server-training'
import testManagement, {
testManagementId,
createModel as testManagementModel
} from '@hcengineering/model-test-management'
import { mailId, createModel as mailModel } from '@hcengineering/model-mail'
import { hulyMailId, createModel as hulyMailModel } from '@hcengineering/model-huly-mail'
import aiAssistant, { aiAssistantId, createModel as aiAssistantModel } from '@hcengineering/model-ai-assistant'
import trainings, { trainingId, createModel as trainingModel } from '@hcengineering/model-training'
import { achievementId, createModel as achievementModel } from '@hcengineering/model-achievement'
import { billingId, createModel as billingModel } from '@hcengineering/model-billing'
import chat, { chatId, createModel as chatModel } from '@hcengineering/model-chat'
import { communicationId, createModel as communicationModel } from '@hcengineering/model-communication'
import { emojiId, createModel as emojiModel } from '@hcengineering/model-emoji'
import inbox, { inboxId, createModel as inboxModel } from '@hcengineering/model-inbox'
import { presenceId, createModel as presenceModel } from '@hcengineering/model-presence'
import processes, { processId, createModel as processModel } from '@hcengineering/model-process'
import {
serverDocumentsId,
createModel as serverDocumentsModel
} from '@hcengineering/model-server-controlled-documents'
import survey, { surveyId, createModel as surveyModel } from '@hcengineering/model-survey'
import { presenceId, createModel as presenceModel } from '@hcengineering/model-presence'
import chat, { chatId, createModel as chatModel } from '@hcengineering/model-chat'
import processes, { processId, createModel as processModel } from '@hcengineering/model-process'
import inbox, { createModel as inboxModel, inboxId } from '@hcengineering/model-inbox'
import { achievementId, createModel as achievementModel } from '@hcengineering/model-achievement'
import { emojiId, createModel as emojiModel } from '@hcengineering/model-emoji'
import { billingId, createModel as billingModel } from '@hcengineering/model-billing'
import { communicationId, createModel as communicationModel } from '@hcengineering/model-communication'
import { type Plugin } from '@hcengineering/platform'
interface ConfigurablePlugin extends Omit<Data<PluginConfiguration>, 'pluginId' | 'transactions'> {}
@@ -161,7 +162,8 @@ export default function buildModel (): Builder {
contact.class.ChannelProvider,
setting.class.IntegrationType,
setting.class.WorkspaceSettingCategory,
setting.class.SettingsCategory
setting.class.SettingsCategory,
workbench.class.Widget
]
const builders: BuilderConfig[] = [
@@ -513,6 +515,19 @@ export default function buildModel (): Builder {
classFilter: defaultFilter
}
],
[
ratingModel,
ratingId,
{
label: rating.string.Rating,
description: rating.string.Rating,
icon: rating.icon.Rating,
hidden: false,
enabled: false,
beta: true,
classFilter: defaultFilter
}
],
[serverCoreModel, serverCoreId],
[serverAttachmentModel, serverAttachmentId],
+20 -18
View File
@@ -16,54 +16,56 @@
// Import migrate operations.
import { type MigrateOperation } from '@hcengineering/model'
import { activityOperation } from '@hcengineering/model-activity'
import { aiBotId, aiBotOperation } from '@hcengineering/model-ai-bot'
import { analyticsCollectorOperation } from '@hcengineering/model-analytics-collector'
import { attachmentOperation } from '@hcengineering/model-attachment'
import { bitrixOperation } from '@hcengineering/model-bitrix'
import { boardOperation } from '@hcengineering/model-board'
import { calendarOperation } from '@hcengineering/model-calendar'
import { cardOperation } from '@hcengineering/model-card'
import { chatId, chatOperation } from '@hcengineering/model-chat'
import { chunterOperation } from '@hcengineering/model-chunter'
import { communicationId, communicationOperation } from '@hcengineering/model-communication'
import { contactOperation } from '@hcengineering/model-contact'
import { documentsOperation } from '@hcengineering/model-controlled-documents'
import { coreOperation } from '@hcengineering/model-core'
import { documentOperation } from '@hcengineering/model-document'
import { driveOperation } from '@hcengineering/model-drive'
import { githubOperation, githubOperationPreTime } from '@hcengineering/model-github'
import { gmailOperation } from '@hcengineering/model-gmail'
import { guestOperation } from '@hcengineering/model-guest'
import { hrOperation } from '@hcengineering/model-hr'
import { inboxId, inboxOperation } from '@hcengineering/model-inbox'
import { inventoryOperation } from '@hcengineering/model-inventory'
import { leadOperation } from '@hcengineering/model-lead'
import { loveId, loveOperation } from '@hcengineering/model-love'
import { notificationOperation } from '@hcengineering/model-notification'
import { preferenceOperation } from '@hcengineering/model-preference'
import { processId, processOperation } from '@hcengineering/model-process'
import { productsOperation } from '@hcengineering/model-products'
import { questionsOperation } from '@hcengineering/model-questions'
import { ratingOperation } from '@hcengineering/model-rating'
import { recorderId, recorderOperation } from '@hcengineering/model-recorder'
import { recruitOperation } from '@hcengineering/model-recruit'
import { requestOperation } from '@hcengineering/model-request'
import { activityServerOperation } from '@hcengineering/model-server-activity'
import { settingOperation } from '@hcengineering/model-setting'
import { surveyOperation } from '@hcengineering/model-survey'
import { tagsOperation } from '@hcengineering/model-tags'
import { taskOperation } from '@hcengineering/model-task'
import { telegramOperation } from '@hcengineering/model-telegram'
import { templatesOperation } from '@hcengineering/model-templates'
import { testManagementOperation } from '@hcengineering/model-test-management'
import { textEditorOperation } from '@hcengineering/model-text-editor'
import { timeOperation } from '@hcengineering/model-time'
import { trackerOperation } from '@hcengineering/model-tracker'
import { viewOperation } from '@hcengineering/model-view'
import { textEditorOperation } from '@hcengineering/model-text-editor'
import { githubOperation, githubOperationPreTime } from '@hcengineering/model-github'
import { loveId, loveOperation } from '@hcengineering/model-love'
import { questionsOperation } from '@hcengineering/model-questions'
import { trainingOperation } from '@hcengineering/model-training'
import { documentsOperation } from '@hcengineering/model-controlled-documents'
import { productsOperation } from '@hcengineering/model-products'
import { requestOperation } from '@hcengineering/model-request'
import { analyticsCollectorOperation } from '@hcengineering/model-analytics-collector'
import { viewOperation } from '@hcengineering/model-view'
import { workbenchOperation } from '@hcengineering/model-workbench'
import { testManagementOperation } from '@hcengineering/model-test-management'
import { surveyOperation } from '@hcengineering/model-survey'
import { cardOperation } from '@hcengineering/model-card'
import { aiBotId, aiBotOperation } from '@hcengineering/model-ai-bot'
import { chatId, chatOperation } from '@hcengineering/model-chat'
import { inboxId, inboxOperation } from '@hcengineering/model-inbox'
import { processId, processOperation } from '@hcengineering/model-process'
import { communicationId, communicationOperation } from '@hcengineering/model-communication'
import { recorderId, recorderOperation } from '@hcengineering/model-recorder'
export const migrateOperations: [string, MigrateOperation][] = [
['core', coreOperation],
['rating', ratingOperation],
['activity', activityOperation],
['card', cardOperation],
['chunter', chunterOperation],
+9 -22
View File
@@ -15,23 +15,17 @@
import activity from '@hcengineering/activity'
import type {
ClassCollaborators,
CollectionSize,
MarkupBlobRef,
Domain,
MarkupBlobRef,
Rank,
Ref,
Role,
RolesAssignment,
ClassCollaborators
RolesAssignment
} from '@hcengineering/core'
import { AccountUuid, AccountRole, IndexKind } from '@hcengineering/core'
import {
type Document,
type DocumentSnapshot,
type SavedDocument,
type Teamspace,
documentId
} from '@hcengineering/document'
import { AccountRole, AccountUuid, IndexKind } from '@hcengineering/core'
import { type Document, type DocumentSnapshot, type Teamspace, documentId } from '@hcengineering/document'
import {
type Builder,
Collection,
@@ -41,19 +35,18 @@ import {
Model,
Prop,
ReadOnly,
TypeAccountUuid,
TypeCollaborativeDoc,
TypeNumber,
TypeRef,
TypeString,
UX,
TypeAccountUuid
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import core, { TDoc, TTypedSpace } from '@hcengineering/model-core'
import { createPublicLinkAction } from '@hcengineering/model-guest'
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import tracker from '@hcengineering/model-tracker'
import view, { actionTemplates, createAction } from '@hcengineering/model-view'
@@ -62,8 +55,8 @@ import notification from '@hcengineering/notification'
import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
import tags from '@hcengineering/tags'
import time, { type ToDo, type Todoable } from '@hcengineering/time'
import document from './plugin'
import { definePermissions } from './permissions'
import document from './plugin'
export { documentId } from '@hcengineering/document'
@@ -148,12 +141,6 @@ export class TDocumentSnapshot extends TDoc implements DocumentSnapshot {
parent!: Ref<Document>
}
@Model(document.class.SavedDocument, preference.class.Preference)
export class TSavedDocument extends TPreference implements SavedDocument {
@Prop(TypeRef(document.class.Document), document.string.SavedDocuments)
declare attachedTo: Ref<Document>
}
@Model(document.class.Teamspace, core.class.TypedSpace)
@UX(document.string.Teamspace, document.icon.Teamspace, 'Teamspace', 'name')
export class TTeamspace extends TTypedSpace implements Teamspace {}
@@ -281,7 +268,7 @@ function defineTeamspace (builder: Builder): void {
}
function defineDocument (builder: Builder): void {
builder.createModel(TDocument, TDocumentSnapshot, TSavedDocument, TDefaultTeamspaceTypeData)
builder.createModel(TDocument, TDocumentSnapshot, TDefaultTeamspaceTypeData)
builder.mixin(document.class.Document, core.class.Class, time.mixin.ItemPresenter, {
presenter: document.component.DocumentToDoPresenter
+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"
}
+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"]
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@hcengineering/model-rating",
"version": "0.7.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",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"test": "jest --passWithNoTests --silent --forceExit"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.7.19",
"@typescript-eslint/eslint-plugin": "^6.21.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.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"@types/node": "^22.15.29",
"jest": "^29.7.0",
"@types/jest": "^29.5.5",
"ts-jest": "^29.1.1"
},
"dependencies": {
"@hcengineering/activity": "^0.7.0",
"@hcengineering/contact": "^0.7.0",
"@hcengineering/core": "^0.7.10",
"@hcengineering/model": "^0.7.6",
"@hcengineering/platform": "^0.7.5",
"@hcengineering/rating": "^0.7.0",
"@hcengineering/model-core": "^0.7.0",
"@hcengineering/model-presentation": "^0.7.0",
"@hcengineering/presentation": "^0.7.0",
"@hcengineering/view": "^0.7.0",
"@hcengineering/ui": "^0.7.0",
"@hcengineering/workbench": "^0.7.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
// 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 Builder } from '@hcengineering/model'
export function createActions (builder: Builder): void {}
+101
View File
@@ -0,0 +1,101 @@
// 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, { type AccountUuid, type PersonId } from '@hcengineering/core'
import { type Builder, Model } from '@hcengineering/model'
import { TAttachedDoc, TDoc } from '@hcengineering/model-core'
import presentation from '@hcengineering/model-presentation'
import {
DOMAIN_PERSON_RATING,
DOMAIN_RATING_REACTION,
type DateCUD,
type DocReaction,
type PersonRating,
type ReactionKind
} from '@hcengineering/rating'
import view from '@hcengineering/view'
import workbench, { WidgetType } from '@hcengineering/workbench'
import { createActions } from './actions'
import rating from './plugin'
export { ratingId } from '@hcengineering/rating'
export { ratingOperation } from './migration'
@Model(rating.class.DocReaction, core.class.Doc, DOMAIN_RATING_REACTION)
export class TDocReaction extends TAttachedDoc implements DocReaction {
// A reaction type identifier
reactionType!: ReactionKind
value!: number // +1, -1, 0..10 etc.
}
@Model(rating.class.PersonRating, core.class.Doc, DOMAIN_PERSON_RATING)
export class TPersonRating extends TDoc implements PersonRating {
accountId!: AccountUuid
// A current person rating
rating!: number // Could be re calculated from values.
// Document authoring statistics
months!: DateCUD[] // Hold per month authoring stats
days!: DateCUD[] // Hold per day authoring stats
hours!: DateCUD[] // Hold per hour authoring stats
stats!: Record<string, [number, number, number]> // create/update/delete Hold per domain authoring stats
socialIds!: Record<PersonId, number>
rageOperations!: number
// A special stats, stars on account and stars on documents, commentas authored by person
starsEarned!: number // All author document stars.
reactionsEarned!: number // All author document reactions
// How many messages person created
messages!: number
stars!: number // Stars spend
reactions!: number // Reactions created
}
export * from './migration'
export function createModel (builder: Builder): void {
builder.createModel(
// TDocRating,
TPersonRating,
TDocReaction
)
createActions(builder)
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: view.extensions.EditDocTitleExtension,
component: rating.component.RatingEditor
})
builder.createDoc(
workbench.class.Widget,
core.space.Model,
{
label: rating.string.RatingWidget,
type: WidgetType.Fixed,
icon: rating.icon.Rating,
component: rating.component.RatingWidget
},
rating.ids.RatingWidget
)
builder.mixin(rating.class.DocReaction, core.class.Class, view.mixin.ObjectPresenter, {
presenter: rating.component.DocReactionPresenter
})
}
export default rating
+32
View File
@@ -0,0 +1,32 @@
//
// 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 MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
tryMigrate,
tryUpgrade
} from '@hcengineering/model'
import { ratingId } from '@hcengineering/rating'
export const ratingOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {
await tryMigrate(mode, client, ratingId, [])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {
await tryUpgrade(mode, state, client, ratingId, [])
}
}
+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 {} from '@hcengineering/core'
import { mergeIds } from '@hcengineering/platform'
import rating, { ratingId } from '@hcengineering/rating'
import type { AnyComponent } from '@hcengineering/ui/src/types'
export default mergeIds(ratingId, rating, {
component: {
RatingEditor: '' as AnyComponent
}
})
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
+1
View File
@@ -171,6 +171,7 @@ export default plugin(presentationId, {
Token: '' as Metadata<string>,
Endpoint: '' as Metadata<string>,
WorkspaceUuid: '' as Metadata<WorkspaceUuid>,
WorkspaceName: '' as Metadata<string>,
WorkspaceDataId: '' as Metadata<WorkspaceDataId>,
FrontUrl: '' as Asset,
LinkPreviewUrl: '' as Metadata<string>,
@@ -32,7 +32,12 @@
deviceOptionsStore as deviceInfo
} from '@hcengineering/ui'
import presence from '@hcengineering/presence'
import { createQuery, createNotificationContextsQuery, getClient } from '@hcengineering/presentation'
import {
createQuery,
createNotificationContextsQuery,
getClient,
ComponentExtensions
} from '@hcengineering/presentation'
import { showMenu } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { NotificationContext } from '@hcengineering/communication-types'
@@ -188,6 +193,10 @@
{doc.title}
{/if}
</div>
<ComponentExtensions
extension={view.extensions.EditDocTitleExtension}
props={{ size: 'medium', kind: 'ghost', _id, _class: doc._class, value: doc, readonly }}
/>
</svelte:fragment>
<svelte:fragment slot="presence">
+2 -1
View File
@@ -64,6 +64,7 @@
"@hcengineering/account-client": "^0.7.7",
"@hcengineering/achievement": "^0.7.0",
"svelte": "^4.2.20",
"crypto-js": "^4.2.0"
"crypto-js": "^4.2.0",
"@hcengineering/rating": "^0.7.0"
}
}
@@ -15,16 +15,18 @@
-->
<script lang="ts">
import { Channel, Person, combineName, getCurrentEmployee, getFirstName, getLastName } from '@hcengineering/contact'
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
import { AttributeEditor, createQuery, getClient } from '@hcengineering/presentation'
import { AccountRole, Ref, getCurrentAccount, hasAccountRole, type AccountUuid } from '@hcengineering/core'
import { AttributeEditor, createQuery, getClient, hasResource } from '@hcengineering/presentation'
import type { PersonRating } from '@hcengineering/rating'
import ratingPlugin from '@hcengineering/rating'
import setting, { IntegrationType } from '@hcengineering/setting'
import { EditBox, FocusHandler, Scroller, createFocusManager } from '@hcengineering/ui'
import { Component, EditBox, FocusHandler, Scroller, createFocusManager } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import contact from '../plugin'
import ChannelsEditor from './ChannelsEditor.svelte'
import EditableAvatar from './EditableAvatar.svelte'
import Avatar from './Avatar.svelte'
import ChannelsDropdown from './ChannelsDropdown.svelte'
import ChannelsEditor from './ChannelsEditor.svelte'
import EditableAvatar from './EditableAvatar.svelte'
export let object: Person
export let readonly: boolean = false
@@ -92,13 +94,26 @@
}
const manager = createFocusManager()
const levelQuery = createQuery()
let personRating: PersonRating | undefined
$: if (object.personUuid !== undefined) {
levelQuery.query(ratingPlugin.class.PersonRating, { accountId: object.personUuid as AccountUuid }, (res) => {
personRating = res[0]
})
} else {
levelQuery.unsubscribe()
personRating = undefined
}
</script>
<FocusHandler {manager} />
{#if object !== undefined}
<div class="flex-row-stretch flex-grow">
<div class="flex-no-shrink mr-8">
<div class="flex-no-shrink mr-8 flex-col flex-row-center">
{#key object}
{#if editable}
<EditableAvatar
@@ -111,6 +126,14 @@
{:else}
<Avatar person={object} size={'x-large'} name={object.name} />
{/if}
{#if personRating != null && hasResource(ratingPlugin.component.RatingRing)}
<div class="flex-row-center mt-2">
<Component
is={ratingPlugin.component.RatingRing}
props={{ rating: personRating.rating, showValues: true }}
/>
</div>
{/if}
{/key}
</div>
<div class="flex-grow flex-col">
@@ -166,6 +189,11 @@
</Scroller>
</div>
</div>
{#if personRating != null && hasResource(ratingPlugin.component.RatingRing)}
<div class="flex-row-center mt-2">
<Component is={ratingPlugin.component.RatingActivities} props={{ rating: personRating }} />
</div>
{/if}
{/if}
<style lang="scss">
@@ -15,23 +15,26 @@
<script lang="ts">
import { Employee, Person } from '@hcengineering/contact'
import { AccountUuid, Class, Doc, Ref } from '@hcengineering/core'
import { ButtonIcon, navigate } from '@hcengineering/ui'
import { ComponentExtensions, createQuery, getClient, hasResource } from '@hcengineering/presentation'
import { ButtonIcon, Component, 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 } from '../../utils'
import { getPersonTimezone } from './utils'
import rating, { type PersonRating } from '@hcengineering/rating'
import { EmployeePresenter, getPersonByPersonRefStore } from '../../index'
import TimePresenter from './TimePresenter.svelte'
import contact from '../../plugin'
import { employeeByIdStore } from '../../utils'
import Avatar from '../Avatar.svelte'
import DeactivatedHeader from './DeactivatedHeader.svelte'
import ModernProfilePopup from './ModernProfilePopup.svelte'
import TimePresenter from './TimePresenter.svelte'
import { getPersonTimezone } from './utils'
export let _id: Ref<Employee>
export let disabled: boolean = false
const hasRating = hasResource(rating.component.RatingRing)
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -44,6 +47,19 @@
$: isEmployee = $employeeByIdStore.has(_id)
$: void loadPersonTimezone(employee)
const levelQuery = createQuery()
let personRating: PersonRating | undefined
$: if (employee?.personUuid != null) {
levelQuery.query(rating.class.PersonRating, { accountId: employee?.personUuid as AccountUuid }, (res) => {
personRating = res[0]
})
} else {
personRating = undefined
levelQuery.unsubscribe()
}
async function viewProfile (): Promise<void> {
if (employee === undefined) return
const panelComponent = hierarchy.classHierarchyMixin(employee._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
@@ -94,6 +110,14 @@
<div class="flex-col flex-gap-0-5 pl-1">
<div class="status-container" />
<EmployeePresenter value={employee} shouldShowAvatar={false} showPopup={false} compact accent />
{#if hasRating}
<div class="flex-row-center text-sm">
<Component
is={rating.component.RatingRing}
props={{ rating: personRating?.rating ?? 0, showValues: false }}
/>
</div>
{/if}
<span class="flex-presenter cursor-default">
<TimePresenter {timezone} />
</span>
@@ -22,7 +22,13 @@
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { IconWithEmoji, copyTextToClipboard, createQuery, getClient } from '@hcengineering/presentation'
import {
ComponentExtensions,
IconWithEmoji,
copyTextToClipboard,
createQuery,
getClient
} from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import { Heading } from '@hcengineering/text-editor'
import { TableOfContents } from '@hcengineering/text-editor-resources'
@@ -52,7 +58,7 @@
} from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { starDocument, unlockContent, unstarDocument } from '..'
import { unlockContent } from '..'
import document from '../plugin'
import { getDocumentUrl } from '../utils'
import DocumentEditor from './DocumentEditor.svelte'
@@ -108,17 +114,6 @@
clearTimeout(copyTimeout)
})
const starredQuery = createQuery()
let isStarred = false
$: starredQuery.query(
document.class.SavedDocument,
{ attachedTo: _id },
(res) => {
isStarred = res.length !== 0
},
{ limit: 1 }
)
async function createEmbedding (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
if (doc === undefined) {
return undefined
@@ -221,18 +216,6 @@
]
let selectedAside: string | boolean = false
$: starAction = isStarred
? {
icon: document.icon.Starred,
label: document.string.Unstar,
action: () => doc !== undefined && unstarDocument(doc)
}
: {
icon: document.icon.Star,
label: document.string.Star,
action: () => doc !== undefined && starDocument(doc)
}
$: actions = [
{
icon: view.icon.CopyId,
@@ -249,8 +232,7 @@
}, 2000)
}
}
},
starAction
}
]
let editor: DocumentEditor
@@ -323,6 +305,12 @@
</svelte:fragment>
<svelte:fragment slot="utils">
{#if doc}
<ComponentExtensions
extension={view.extensions.EditDocTitleExtension}
props={{ size: 'medium', kind: 'ghost', _id: doc._id, _class: doc._class, value: doc, readonly }}
/>
{/if}
{#if !$restrictionStore.disableActions}
<Button
id="btn-doc-title-open-more"
+2 -19
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import core, {
import {
generateId,
getCurrentAccount,
type Class,
@@ -31,8 +31,8 @@ import { openDoc } from '@hcengineering/view-resources'
import CreateDocument from './components/CreateDocument.svelte'
import DocumentIcon from './components/DocumentIcon.svelte'
import DocumentItem from './components/DocumentItem.svelte'
import DocumentInlineEditor from './components/DocumentInlineEditor.svelte'
import DocumentItem from './components/DocumentItem.svelte'
import DocumentPresenter from './components/DocumentPresenter.svelte'
import DocumentSearchIcon from './components/DocumentSearchIcon.svelte'
import DocumentToDoPresenter from './components/DocumentToDoPresenter.svelte'
@@ -113,23 +113,6 @@ async function editTeamspace (teamspace: Teamspace | undefined): Promise<void> {
}
}
export async function starDocument (doc: Document): Promise<void> {
const client = getClient()
await client.createDoc(document.class.SavedDocument, core.space.Workspace, {
attachedTo: doc._id
})
}
export async function unstarDocument (doc: Document): Promise<void> {
const client = getClient()
const current = await client.findOne(document.class.SavedDocument, { attachedTo: doc._id })
if (current !== undefined) {
await client.remove(current)
}
}
export async function lockContent (doc: Document | Document[]): Promise<void> {
const client = getClient()
const me = getCurrentAccount()
+1 -2
View File
@@ -14,7 +14,7 @@
//
import { type Client, type Doc, type Ref } from '@hcengineering/core'
import document, { type Document, documentId } from '@hcengineering/document'
import document, { documentId, type Document } from '@hcengineering/document'
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
import { type AnyComponent, type Location } from '@hcengineering/ui/src/types'
@@ -50,7 +50,6 @@ export default mergeIds(documentId, document, {
Authors: '' as IntlString,
Backlinks: '' as IntlString,
History: '' as IntlString,
SavedDocuments: '' as IntlString,
AddLabel: '' as IntlString,
NewTeamspace: '' as IntlString,
CreateTeamspace: '' as IntlString,
+2 -3
View File
@@ -13,13 +13,13 @@
// limitations under the License.
//
import type { Class, Doc, Mixin, Ref, SpaceType, SpaceTypeDescriptor, Permission } from '@hcengineering/core'
import type { Class, Doc, Mixin, Permission, Ref, SpaceType, SpaceTypeDescriptor } from '@hcengineering/core'
import { NotificationGroup, NotificationType } from '@hcengineering/notification'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import type { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui/src/types'
import { Action } from '@hcengineering/view'
import { Document, DocumentSnapshot, SavedDocument, Teamspace } from './types'
import { Document, DocumentSnapshot, Teamspace } from './types'
/**
* @public
@@ -33,7 +33,6 @@ export const documentPlugin = plugin(documentId, {
class: {
Document: '' as Ref<Class<Document>>,
DocumentSnapshot: '' as Ref<Class<DocumentSnapshot>>,
SavedDocument: '' as Ref<Class<SavedDocument>>,
Teamspace: '' as Ref<Class<Teamspace>>
},
mixin: {
-6
View File
@@ -14,7 +14,6 @@
//
import { AccountUuid, Doc, MarkupBlobRef, Rank, Ref, TypedSpace } from '@hcengineering/core'
import { Preference } from '@hcengineering/preference'
import { IconProps } from '@hcengineering/view'
/** @public */
@@ -45,8 +44,3 @@ export interface DocumentSnapshot extends Doc {
content: MarkupBlobRef
parent: Ref<Document>
}
/** @public */
export interface SavedDocument extends Preference {
attachedTo: Ref<Document>
}
+1
View File
@@ -72,6 +72,7 @@ export async function connect (title: string): Promise<Client | undefined> {
setMetadata(presentation.metadata.Token, exchangedToken)
setMetadata(presentation.metadata.WorkspaceUuid, workspaceLoginInfo.workspace)
setMetadata(presentation.metadata.WorkspaceName, workspaceLoginInfo.name ?? workspaceLoginInfo.workspaceUrl)
setMetadata(presentation.metadata.WorkspaceDataId, workspaceLoginInfo.workspaceDataId)
setMetadata(presentation.metadata.Endpoint, workspaceLoginInfo.endpoint)
+3 -1
View File
@@ -115,7 +115,9 @@
"ExecutionInitiator": "Iniciátor provedení",
"ExecutionStarted": "Provedení spuštěno",
"Filter": "Filtr",
"FirstMatchValue": "První odpovídající hodnota"
"FirstMatchValue": "První odpovídající hodnota",
"ConfigLabel": "Konfigurace procesů",
"ConfigDescription": "Definujte procesy pro automatizaci pracovních postupů a obchodních procesů."
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
+3 -1
View File
@@ -115,7 +115,9 @@
"ExecutionInitiator": "Ausführungsinitiator",
"ExecutionStarted": "Ausführung gestartet",
"Filter": "Filter",
"FirstMatchValue": "Erster passender Wert"
"FirstMatchValue": "Erster passender Wert",
"ConfigLabel": "Prozesskonfiguration",
"ConfigDescription": "Definieren Sie Prozesse, um Workflows und Geschäftsprozesse zu automatisieren."
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
+3 -1
View File
@@ -120,7 +120,9 @@
"ExecutionInitiator": "Execution initiator",
"ExecutionStarted": "Execution started",
"Filter": "Filter",
"FirstMatchValue": "First match value"
"FirstMatchValue": "First match value",
"ConfigLabel": "Process configuration",
"ConfigDescription": "Define processes to automate workflows and business processes."
},
"error": {
"MethodNotFound": "Method not found: {methodId}",
+3 -1
View File
@@ -120,7 +120,9 @@
"ExecutionInitiator": "Iniciador de Ejecución",
"ExecutionStarted": "Ejecución Iniciada",
"Filter": "Filtro",
"FirstMatchValue": "Primer valor coincidente"
"FirstMatchValue": "Primer valor coincidente",
"ConfigLabel": "Configuración de procesos",
"ConfigDescription": "Defina procesos para automatizar flujos de trabajo y procesos comerciales."
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
+4 -2
View File
@@ -120,7 +120,9 @@
"ExecutionInitiator": "Initiateur d'exécution",
"ExecutionStarted": "Exécution démarrée",
"Filter": "Filtrer",
"FirstMatchValue": "Première valeur correspondante"
"FirstMatchValue": "Première valeur correspondante",
"ConfigLabel": "Configuration des processus",
"ConfigDescription": "Définissez des processus pour automatiser les flux de travail et les processus métier."
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
@@ -138,4 +140,4 @@
"RequiredParamsNotProvided": "Paramètres requis non fournis : {params}",
"TooDeepTransitionRecursion": "Récursion de transition trop profonde"
}
}
}
+4 -2
View File
@@ -32,7 +32,7 @@
"LowerCase": "Minuscolo",
"Trim": "Taglia",
"FirstValue": "Primo valore",
"LastValue": "Ultimo valore",
"LastValue": "Ultimo valore",
"Random": "Casuale",
"AllProcesses": "Tutti i processi",
"MyProcesses": "I miei processi",
@@ -120,7 +120,9 @@
"ExecutionInitiator": "Iniziatore dell'esecuzione",
"ExecutionStarted": "Esecuzione avviata",
"Filter": "Filtro",
"FirstMatchValue": "Primo valore corrispondente"
"FirstMatchValue": "Primo valore corrispondente",
"ConfigLabel": "Configurazione dei processi",
"ConfigDescription": "Definisci i processi per automatizzare i flussi di lavoro e i processi aziendali."
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
+3 -1
View File
@@ -119,7 +119,9 @@
"ExecutionInitiator": "実行の発起人",
"ExecutionStarted": "実行が開始されました",
"Filter": "フィルター",
"FirstMatchValue": "最初の一致する値"
"FirstMatchValue": "最初の一致する値",
"ConfigLabel": "プロセス構成",
"ConfigDescription": "ワークフローやビジネスプロセスを自動化するためのプロセスを定義します。"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
+3 -1
View File
@@ -120,7 +120,9 @@
"ExecutionInitiator": "Iniciador de Execução",
"ExecutionStarted": "Execução Iniciada",
"Filter": "Filtrar",
"FirstMatchValue": "Primeiro Valor Correspondente"
"FirstMatchValue": "Primeiro Valor Correspondente",
"ConfigLabel": "Configuração de processos",
"ConfigDescription": "Defina processos para automatizar fluxos de trabalho e processos de negócios."
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+3 -1
View File
@@ -120,7 +120,9 @@
"ExecutionInitiator": "Инициатор выполнения",
"ExecutionStarted": "Выполнение начато",
"Filter": "Фильтр",
"FirstMatchValue": "Первое подходящее значение"
"FirstMatchValue": "Первое подходящее значение",
"ConfigLabel": "Конфигурация процессов",
"ConfigDescription": "Определяйте процессы для автоматизации рабочих процессов и бизнес-процессов."
},
"error": {
"MethodNotFound": "Метод не найден: {methodId}",
+3 -1
View File
@@ -117,7 +117,9 @@
"ProcessFinished": "\"{process}\" süreci \"{state}\" durumunda tamamlandı",
"NewProcessToDo": "Yeni süreç Eylem öğesi",
"Filter": "Filtre",
"FirstMatchValue": "İlk eşleşen değer"
"FirstMatchValue": "İlk eşleşen değer",
"ConfigLabel": "Süreç yapılandırması",
"ConfigDescription": "İş akışlarını ve iş süreçlerini otomatikleştirmek için süreçler tanımlayın."
},
"error": {
"MethodNotFound": "Metod bulunamadı: {methodId}",
+3 -1
View File
@@ -120,7 +120,9 @@
"ExecutionInitiator": "执行发起者",
"ExecutionStarted": "执行已启动",
"Filter": "过滤",
"FirstMatchValue": "第一个匹配值"
"FirstMatchValue": "第一个匹配值",
"ConfigLabel": "流程配置",
"ConfigDescription": "定义流程以自动化工作流和业务流程。"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
+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'
}
}
+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="rating" viewBox="0 0 90 90">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;">
<path d="M 45 0.24 L 58.916 24.19 c 0.092 0.185 0.268 0.313 0.472 0.343 l 24.357 3.539 c 1.279 0.186 2.322 1.065 2.721 2.294 c 0.4 1.23 0.073 2.554 -0.853 3.456 l -17.624 17.18 c -0.148 0.144 -0.215 0.351 -0.181 0.554 l 4.161 24.258 c 0.218 1.274 -0.295 2.538 -1.341 3.297 c -1.045 0.76 -2.405 0.858 -3.552 0.258 L 45.291 67.916 C 45.1 67.822 44.9 67.822 44.709 67.916 L 22.923 79.371 c -1.147 0.6 -2.507 0.502 -3.552 -0.258 c -1.046 -0.759 -1.559 -2.023 -1.341 -3.297 l 4.161 -24.258 c 0.034 -0.203 -0.033 -0.41 -0.181 -0.554 L 4.387 33.824 c -0.926 -0.902 -1.253 -2.226 -0.853 -3.456 c 0.399 -1.229 1.442 -2.108 2.721 -2.294 l 24.357 -3.539 c 0.204 -0.03 0.38 -0.158 0.472 -0.343 L 41.977 2.119 C 42.549 0.96 43.707 0.24 45 0.24 z" style="stroke: currentColor; stroke-width: 5; stroke-dasharray: none; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
</g>
</symbol>
<symbol id="star-yellow" viewBox="0 0 256 256">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(3.2 3.2)">
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c -1.293 0 -2.451 0.72 -3.023 1.879 L 31.084 24.19 c -0.092 0.185 -0.268 0.313 -0.472 0.343 L 6.255 28.073 c -1.279 0.186 -2.322 1.065 -2.721 2.294 c -0.4 1.23 -0.073 2.554 0.853 3.456 l 17.624 17.18 c 0.148 0.144 0.215 0.351 0.181 0.554 l -4.161 24.258 c -0.218 1.274 0.295 2.538 1.341 3.297 c 1.045 0.76 2.405 0.858 3.552 0.258 l 21.786 -11.455 C 44.8 67.869 44.9 67.845 45 67.845 C 51.218 48.239 49.905 25.085 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,200,80); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c 1.293 0 2.451 0.72 3.023 1.879 L 58.916 24.19 c 0.092 0.185 0.268 0.313 0.472 0.343 l 24.357 3.539 c 1.279 0.186 2.322 1.065 2.721 2.294 c 0.4 1.23 0.073 2.554 -0.853 3.456 l -17.624 17.18 c -0.148 0.144 -0.215 0.351 -0.181 0.554 l 4.161 24.258 c 0.218 1.274 -0.295 2.538 -1.341 3.297 c -1.045 0.76 -2.405 0.858 -3.552 0.258 L 45.291 67.916 C 45.2 67.869 45.1 67.845 45 67.845 C 37.673 48.876 39.309 25.564 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,220,100); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
</g>
</symbol>
<symbol id="star-green" viewBox="0 0 256 256">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(3.2 3.2)">
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c -1.293 0 -2.451 0.72 -3.023 1.879 L 31.084 24.19 c -0.092 0.185 -0.268 0.313 -0.472 0.343 L 6.255 28.073 c -1.279 0.186 -2.322 1.065 -2.721 2.294 c -0.4 1.23 -0.073 2.554 0.853 3.456 l 17.624 17.18 c 0.148 0.144 0.215 0.351 0.181 0.554 l -4.161 24.258 c -0.218 1.274 0.295 2.538 1.341 3.297 c 1.045 0.76 2.405 0.858 3.552 0.258 l 21.786 -11.455 C 44.8 67.869 44.9 67.845 45 67.845 C 51.218 48.239 49.905 25.085 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(80,200,120); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c 1.293 0 2.451 0.72 3.023 1.879 L 58.916 24.19 c 0.092 0.185 0.268 0.313 0.472 0.343 l 24.357 3.539 c 1.279 0.186 2.322 1.065 2.721 2.294 c 0.4 1.23 0.073 2.554 -0.853 3.456 l -17.624 17.18 c -0.148 0.144 -0.215 0.351 -0.181 0.554 l 4.161 24.258 c 0.218 1.274 -0.295 2.538 -1.341 3.297 c -1.045 0.76 -2.405 0.858 -3.552 0.258 L 45.291 67.916 C 45.2 67.869 45.1 67.845 45 67.845 C 37.673 48.876 39.309 25.564 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(100,220,140); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
</g>
</symbol>
<symbol id="star-red" viewBox="0 0 256 256">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(3.2 3.2)">
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c -1.293 0 -2.451 0.72 -3.023 1.879 L 31.084 24.19 c -0.092 0.185 -0.268 0.313 -0.472 0.343 L 6.255 28.073 c -1.279 0.186 -2.322 1.065 -2.721 2.294 c -0.4 1.23 -0.073 2.554 0.853 3.456 l 17.624 17.18 c 0.148 0.144 0.215 0.351 0.181 0.554 l -4.161 24.258 c -0.218 1.274 0.295 2.538 1.341 3.297 c 1.045 0.76 2.405 0.858 3.552 0.258 l 21.786 -11.455 C 44.8 67.869 44.9 67.845 45 67.845 C 51.218 48.239 49.905 25.085 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(220,80,80); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c 1.293 0 2.451 0.72 3.023 1.879 L 58.916 24.19 c 0.092 0.185 0.268 0.313 0.472 0.343 l 24.357 3.539 c 1.279 0.186 2.322 1.065 2.721 2.294 c 0.4 1.23 0.073 2.554 -0.853 3.456 l -17.624 17.18 c -0.148 0.144 -0.215 0.351 -0.181 0.554 l 4.161 24.258 c 0.218 1.274 -0.295 2.538 -1.341 3.297 c -1.045 0.76 -2.405 0.858 -3.552 0.258 L 45.291 67.916 C 45.2 67.869 45.1 67.845 45 67.845 C 37.673 48.876 39.309 25.564 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,100,100); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
</g>
</symbol>
<symbol id="star-blue" viewBox="0 0 256 256">
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(3.2 3.2)">
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c -1.293 0 -2.451 0.72 -3.023 1.879 L 31.084 24.19 c -0.092 0.185 -0.268 0.313 -0.472 0.343 L 6.255 28.073 c -1.279 0.186 -2.322 1.065 -2.721 2.294 c -0.4 1.23 -0.073 2.554 0.853 3.456 l 17.624 17.18 c 0.148 0.144 0.215 0.351 0.181 0.554 l -4.161 24.258 c -0.218 1.274 0.295 2.538 1.341 3.297 c 1.045 0.76 2.405 0.858 3.552 0.258 l 21.786 -11.455 C 44.8 67.869 44.9 67.845 45 67.845 C 51.218 48.239 49.905 25.085 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(80,150,220); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
<path d="M 45 0.24 C 45 0.24 45 0.24 45 0.24 c 1.293 0 2.451 0.72 3.023 1.879 L 58.916 24.19 c 0.092 0.185 0.268 0.313 0.472 0.343 l 24.357 3.539 c 1.279 0.186 2.322 1.065 2.721 2.294 c 0.4 1.23 0.073 2.554 -0.853 3.456 l -17.624 17.18 c -0.148 0.144 -0.215 0.351 -0.181 0.554 l 4.161 24.258 c 0.218 1.274 -0.295 2.538 -1.341 3.297 c -1.045 0.76 -2.405 0.858 -3.552 0.258 L 45.291 67.916 C 45.2 67.869 45.1 67.845 45 67.845 C 37.673 48.876 39.309 25.564 45 0.24 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(100,180,255); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
</g>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "assets"
}
+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"]
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Hodnocení",
"RatingWidgetEmpty": "Zatím nejsou žádné označené dokumenty. Chcete-li zde zobrazit položky, přidejte hvězdu k dokumentu (klikněte na reakci hvězdy u libovolného dokumentu).",
"RatingWidget": "Widget hodnocení",
"Level": "Úroveň",
"MonthOps": "{month}: {ops} operací",
"MonthNoOps": "{month}, žádné operace",
"AddStar": "Přidat hvězdu",
"AddStarAria": "Přidat reakci hvězdy"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Bewertung",
"RatingWidgetEmpty": "Noch keine markierten Dokumente. Um Elemente hier zu sehen, füge einem Dokument einen Stern hinzu (klicke auf die Stern-Reaktion bei einem Dokument).",
"RatingWidget": "Bewertungs-Widget",
"Level": "Level",
"MonthOps": "{month}: {ops} Vorgänge",
"MonthNoOps": "{month}, keine Vorgänge",
"AddStar": "Stern hinzufügen",
"AddStarAria": "Stern-Reaktion hinzufügen"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Rating",
"RatingWidget": "Rating widget",
"RatingWidgetEmpty": "No starred documents yet. To see items here, add a star to a document (click the star reaction on any document).",
"Level": "Level",
"MonthOps": "{month}: {ops} ops",
"MonthNoOps": "{month}, No ops",
"AddStar": "Add star",
"AddStarAria": "Add star reaction"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Calificación",
"RatingWidget": "Widget de calificación",
"Level": "Nivel",
"RatingWidgetEmpty": "Aún no hay documentos con estrella. Para ver elementos aquí, añade una estrella a un documento (haz clic en la reacción de estrella en cualquier documento).",
"MonthOps": "{month}: {ops} operaciones",
"MonthNoOps": "{month}, sin operaciones",
"AddStar": "Agregar estrella",
"AddStarAria": "Agregar reacción de estrella"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Note",
"RatingWidgetEmpty": "Aucun document marqué d'une étoile pour le moment. Pour afficher des éléments ici, ajoutez une étoile à un document (cliquez sur la réaction étoile sur un document).",
"RatingWidget": "Widget de notation",
"Level": "Niveau",
"MonthOps": "{month} : {ops} opérations",
"MonthNoOps": "{month}, aucune opération",
"AddStar": "Ajouter une étoile",
"AddStarAria": "Ajouter une réaction étoile"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Valutazione",
"RatingWidgetEmpty": "Non ci sono ancora documenti contrassegnati con una stella. Per vederli qui, aggiungi una stella a un documento (clicca sulla reazione a stella su un documento).",
"RatingWidget": "Widget di valutazione",
"Level": "Livello",
"MonthOps": "{month}: {ops} operazioni",
"MonthNoOps": "{month}, nessuna operazione",
"AddStar": "Aggiungi stella",
"AddStarAria": "Aggiungi reazione stella"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "評価",
"RatingWidgetEmpty": "まだスターが付いたドキュメントはありません。ここに表示するには、ドキュメントにスターを追加してください(ドキュメントのスター反応をクリックしてください)。",
"RatingWidget": "評価ウィジェット",
"Level": "レベル",
"MonthOps": "{month}月: {ops} 操作",
"MonthNoOps": "{month}月、操作なし",
"AddStar": "スターを追加",
"AddStarAria": "スターの反応を追加"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Avaliação",
"RatingWidgetEmpty": "Ainda não há documentos com estrela. Para ver itens aqui, adicione uma estrela a um documento (clique na reação de estrela em qualquer documento).",
"RatingWidget": "Widget de avaliação",
"Level": "Nível",
"MonthOps": "{month}: {ops} operações",
"MonthNoOps": "{month}, sem operações",
"AddStar": "Adicionar estrela",
"AddStarAria": "Adicionar reação de estrela"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Рейтинг",
"RatingWidgetEmpty": "Пока нет документов со звёздочкой. Чтобы увидеть элементы здесь, добавьте звезду к документу (нажмите на реакцию со звездой в любом документе).",
"RatingWidget": "Виджет рейтинга",
"Level": "Уровень",
"MonthOps": "{month}: {ops} операций",
"MonthNoOps": "{month}, операций нет",
"AddStar": "Добавить звезду",
"AddStarAria": "Добавить реакцию со звездой"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "Puan",
"RatingWidgetEmpty": "Henüz yıldızlı belge yok. Burada öğeleri görmek için bir belgeye yıldız ekleyin (herhangi bir belgedeki yıldız tepkisine tıklayın).",
"RatingWidget": "Puan aracı",
"Level": "Seviye",
"MonthOps": "{month}: {ops} işlem",
"MonthNoOps": "{month}, işlem yok",
"AddStar": "Yıldız ekle",
"AddStarAria": "Yıldız tepkisi ekle"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"string": {
"Rating": "评分",
"RatingWidgetEmpty": "尚无加星的文档。要在此处查看项目,请为文档添加星标(在任何文档上点击星形反应)。",
"RatingWidget": "评分组件",
"Level": "等级",
"MonthOps": "{month}月:{ops} 操作",
"MonthNoOps": "{month}月,无操作",
"AddStar": "添加星标",
"AddStarAria": "添加星标反应"
}
}
+69
View File
@@ -0,0 +1,69 @@
{
"name": "@hcengineering/rating-assets",
"version": "0.7.0",
"main": "./lib/index.js",
"types": "./types/index.d.ts",
"files": [
"lib",
"types",
"src",
"assets",
"lang"
],
"author": "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.7.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.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.6.2",
"@types/node": "^22.15.29",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"typescript": "^5.9.3"
},
"dependencies": {
"@hcengineering/platform": "^0.7.5",
"@hcengineering/rating": "^0.7.0"
},
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./lib/index.js"
},
"./lang/*.json": {
"require": "./lang/*.json",
"import": "./lang/*.json"
},
"./lang": {
"require": "./lang",
"import": "./lang"
},
"./assets": {
"require": "./assets",
"import": "./assets"
},
"./assets/*.svg": {
"require": "./assets/*.svg",
"import": "./assets/*.svg"
}
}
}
@@ -0,0 +1,6 @@
import { makeLocalesTest } from '@hcengineering/platform'
it(
'Locales are equale',
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
)
+26
View File
@@ -0,0 +1,26 @@
//
// 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 rating from '@hcengineering/rating'
import { loadMetadata } from '@hcengineering/platform'
const ratingIcon = require('../assets/icon.svg') as string // eslint-disable-line
loadMetadata(rating.icon, {
Rating: `${ratingIcon}#rating`,
StarYellow: `${ratingIcon}#star-yellow`,
StarGreen: `${ratingIcon}#star-green`,
StarBlue: `${ratingIcon}#star-blue`,
StarRed: `${ratingIcon}#star-red`
})
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"types": ["node", "jest"],
"tsBuildInfoFile": ".build/build.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
+4
View File
@@ -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"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_2_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="14.3791" y1="-2.5354" x2="21.6209" y2="38.5354">
<stop offset="5.076140e-03" style="stop-color:#C52229"/>
<stop offset="1" style="stop-color:#802022"/>
</linearGradient>
<rect class="st0" width="36" height="36"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="18" y1="6.2743" x2="18" y2="29.7257">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="0.5066" style="stop-color:#FDFDFD"/>
<stop offset="0.7696" style="stop-color:#F5F5F5"/>
<stop offset="0.9763" style="stop-color:#E8E8E8"/>
<stop offset="1" style="stop-color:#E6E6E6"/>
</linearGradient>
<path class="st1" d="M7.3,9.8c2.8-1.1,7.4-1.3,9.4-1.2l1.3,1.6l1.3-1.6c2-0.1,6.6,0.1,9.4,1.2c-0.4,0.7-2.1,1.8-3,2
c-0.1-1.4-2.1-1.7-4-1.7L18,29.7l-3.7-19.7c-2,0-4,0.3-4,1.7C9.4,11.6,7.7,10.5,7.3,9.8L7.3,9.8z M6,7.8l0.7,1.1
c2.7-1,5.9-1.4,9.3-1.5c1.3,0,2.6,0,3.9,0c3.4,0.1,6.6,0.6,9.3,1.5L30,7.8c-3.2-1.1-6.6-1.4-10-1.5c-1.3,0-2.6,0-4,0
C12.6,6.4,9.2,6.7,6,7.8L6,7.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+5
View File
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "@hcengineering/rating-resources",
"version": "0.7.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile ui",
"build:docs": "api-extractor run --local",
"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.7.19",
"svelte-loader": "^3.2.0",
"sass": "^1.80.0",
"svelte-preprocess": "^5.1.4",
"@typescript-eslint/eslint-plugin": "^6.21.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.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier-plugin-svelte": "^3.4.0",
"prettier": "^3.6.2",
"svelte-check": "^3.6.9",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/analytics": "^0.7.5",
"@hcengineering/platform": "^0.7.5",
"svelte": "^4.2.20",
"@hcengineering/rating": "^0.7.0",
"@hcengineering/ui": "^0.7.0",
"@hcengineering/presentation": "^0.7.0",
"@hcengineering/core": "^0.7.10",
"@hcengineering/view": "^0.7.0",
"@hcengineering/emoji": "^0.7.0",
"@hcengineering/emoji-resources": "^0.7.0",
"@hcengineering/contact": "^0.7.0",
"@hcengineering/contact-resources": "^0.7.0",
"@hcengineering/view-resources": "^0.7.0"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
@@ -0,0 +1,15 @@
<script lang="ts">
import ratingPlugin, { DocReaction, ReactionKind } from '@hcengineering/rating'
import { Icon } from '@hcengineering/ui'
import ReactionPresenter from './ReactionPresenter.svelte'
export let value: DocReaction | null = null
</script>
{#if value}
{#if value.reactionType === ReactionKind.Star}
<Icon icon={ratingPlugin.icon.StarYellow} size={'small'} />
{:else}
<ReactionPresenter emoji={value?.emoji ?? ''} />
{/if}
{/if}
@@ -0,0 +1,53 @@
<!--
// 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 { Ref, type Class, type Doc } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { FixedColumn, ObjectPresenter } from '@hcengineering/view-resources'
import RatingEditor from './RatingEditor.svelte'
import type { DocReaction } from '@hcengineering/rating'
export let _class: Class<Doc>
export let docs: { _id: Ref<Doc>, reactions: DocReaction[] }[] = []
const query = createQuery()
const limit: number = 20
let objects: { doc: Doc, reactions: DocReaction[] }[] = []
$: query.query(
_class._id,
{ _id: { $in: docs.map((it) => it._id) } },
(res) => {
objects = res.map((doc) => ({
doc,
reactions: docs.find((it) => it._id === doc._id)?.reactions ?? []
}))
},
{ limit, total: true }
)
</script>
<div class="ml-2 flex-col">
{#each objects as doc (doc.doc._id)}
<div class="flex-presenter p-1 flex flex-between">
<FixedColumn key={'reactions_link'}>
<ObjectPresenter _class={doc.doc._class} objectId={doc.doc._id} value={doc.doc} noUnderline />
</FixedColumn>
<RatingEditor _class={doc.doc._class} _id={doc.doc._id} reactions={doc.reactions} showMy={false} />
</div>
{/each}
</div>
@@ -0,0 +1,120 @@
<script lang="ts">
import type { PersonRating } from '@hcengineering/rating'
export let rating: PersonRating | undefined
// Sorted months
$: months = [...(rating?.months ?? [])].sort((a, b) => a[0] - b[0])
$: fullYears = Array.from(new Set(months.map((it) => Math.floor(it[0] / 100))))
$: ymin = Math.min(...fullYears)
$: ymax = Math.max(...fullYears)
const monthMax: number = 500
// Color scale: 0 = #ebedf0, low = #c6e48b, mid = #7bc96f, high = #239a3b, max = #196127
function getColor (val: number, max: number): string {
max = Math.max(max, monthMax)
const COLORS = [
'transparent',
'#c6e48b',
'#a0d16e',
'#8ec760',
'#7bc96f',
'#6bb85e',
'#5ba94d',
'#4b9a3c',
'#3b8b2b',
'#2b7c1a',
'#239a3b',
'#1f7a2c',
'#1b7820',
'#177614',
'#156c11',
'#196127',
'#144f0e'
]
if (val <= 0) return COLORS[0]
const idx = 1 + Math.min(COLORS.length - 2, Math.floor((val / max) * (COLORS.length - 1)))
return COLORS[idx]
}
function getMax (year: number, rating?: PersonRating): number {
const monthData = rating?.months?.filter((it) => Math.floor(it[0] / 100) === year)
if (monthData === undefined) return monthMax
return Math.max(...monthData.map((val) => (val[1] ?? 0) + (val[2] ?? 0) + (val[3] ?? 0)))
}
import ratingPlugin from '@hcengineering/rating'
import { tooltip } from '@hcengineering/ui'
// Formatter for localized month names (short form, e.g. 'Jan', 'Feb' or localized equivalent)
const monthFormatter = new Intl.DateTimeFormat(undefined, { month: 'short' })
</script>
{#if rating}
<div class="flex flex-col">
<div class="rating-activities">
<!-- Monthly grid -->
{#each Array.from({ length: ymax - ymin + 1 }, (_, i) => i + ymin) as year}
{@const ymonths = months.filter((it) => Math.floor(it[0] / 100) === year)}
<div class="flex-row flex flex-col p-1">
<div class="flex flex-grow justify-center">
{year}
</div>
<div class="contribution-grid">
{#each Array.from({ length: 12 }, (_, i) => i) as m, i}
{@const monthval = year * 100 + m}
{@const yval = ymonths.find((it) => it[0] === monthval)}
{#if yval}
{@const ops = (yval[1] ?? 0) + (yval[2] ?? 0) + (yval[3] ?? 0)}
{@const monthLabel = monthFormatter.format(new Date(year, i, 1))}
<div
class="contribution-cell"
use:tooltip={{ label: ratingPlugin.string.MonthOps, props: { month: monthLabel, ops: String(ops) } }}
style="background: {getColor(ops, getMax(year, rating))}"
></div>
{:else}
{@const monthLabel = monthFormatter.format(new Date(year, i, 1))}
<div
class="contribution-cell"
use:tooltip={{ label: ratingPlugin.string.MonthNoOps, props: { month: monthLabel } }}
style="background: {getColor(0, monthMax)}"
></div>
{/if}
{/each}
</div>
</div>
{/each}
</div>
</div>
{/if}
<style>
.rating-activities {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 1em;
max-width: fit-content;
max-height: fit-content;
}
.contribution-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2px;
margin-bottom: 1em;
max-width: fit-content;
max-height: fit-content;
}
.contribution-cell {
width: 100%;
height: 10px;
width: 10px;
aspect-ratio: 1/1;
border-radius: 2px;
border: 1px solid #e0e0e0;
transition: background 0.2s;
}
</style>
@@ -0,0 +1,259 @@
<script lang="ts">
import type { Class, Doc, Ref } from '@hcengineering/core'
import core, { getCurrentAccount, groupByArray } from '@hcengineering/core'
import emojiPlugin from '@hcengineering/emoji'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { DocReaction } from '@hcengineering/rating'
import ratingPlugin, { ReactionKind } from '@hcengineering/rating'
import { Icon, showPopup, tooltip } from '@hcengineering/ui'
import ReactionPresenter from './ReactionPresenter.svelte'
import { translateCB } from '@hcengineering/platform'
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
export let showMy = true
export let reactions: DocReaction[] = []
let _reactions = reactions
// Compute reaction groups
$: starReactions = _reactions.filter((r) => r.reactionType === ReactionKind.Star)
$: otherReactions = groupByArray(
_reactions.filter((r) => r.reactionType === ReactionKind.Emoji),
(it) => it.emoji ?? ''
).entries()
$: starCount = starReactions.filter((it) => it.value === 1).length
const account = getCurrentAccount()
const socialIds = new Set(account.fullSocialIds.map((it) => it._id))
const client = getClient()
const reactionsQuery = createQuery()
$: if (reactions.length === 0) {
reactionsQuery.query(ratingPlugin.class.DocReaction, { attachedTo: _id, attachedToClass: _class }, (result) => {
_reactions = result
})
} else {
reactionsQuery.unsubscribe()
_reactions = reactions
}
async function addStarReaction (): Promise<void> {
const existing = _reactions.filter((it) => it.reactionType === ReactionKind.Star && socialIds.has(it.modifiedBy))
if (existing.length > 0) {
for (const e of existing) {
if (e.value === 0) {
await client.update(e, { value: 1 })
} else {
await client.update(e, { value: 0 })
}
}
} else {
await client.addCollection(ratingPlugin.class.DocReaction, core.space.Workspace, _id, _class, '_reactions', {
reactionType: ReactionKind.Star,
value: 1
})
}
}
async function addEmojiReaction (event: MouseEvent): Promise<void> {
showPopup(emojiPlugin.component.EmojiPopup, {}, event.target as HTMLElement, async (emoji) => {
if (emoji?.text === undefined) return
const existing = _reactions.filter(
(it) => it.reactionType === ReactionKind.Emoji && it.emoji === emoji.text && socialIds.has(it.modifiedBy)
)
if (existing.length > 0) {
for (const e of existing) {
await client.remove(e)
}
} else {
await client.addCollection(ratingPlugin.class.DocReaction, core.space.Workspace, _id, _class, '_reactions', {
reactionType: ReactionKind.Emoji,
value: 0,
emoji: emoji.text,
image: emoji.image
})
}
})
}
async function removeReaction (reactions: DocReaction[]): Promise<void> {
for (const reaction of reactions) {
if (account.fullSocialIds.some((it) => it._id === reaction.modifiedBy)) {
await client.removeDoc(reaction._class, reaction.space, reaction._id)
}
}
}
$: hasSelectedStar = _reactions.some(
(it) => it.reactionType === ReactionKind.Star && socialIds.has(it.createdBy ?? it.modifiedBy) && it.value === 1
)
let addStarAria: string | undefined = undefined
$: translateCB(ratingPlugin.string.AddStarAria, {}, undefined, (r) => (addStarAria = r))
</script>
<div class="rating-editor">
<!-- Inline Reactions -->
<div class="reactions-container">
<!-- Star Reactions (Premium) -->
<button
class="star-reaction"
on:click={addStarReaction}
use:tooltip={{ label: ratingPlugin.string.AddStar }}
aria-label={addStarAria}
>
<span class="star-icon">
<Icon icon={hasSelectedStar ? ratingPlugin.icon.StarYellow : ratingPlugin.icon.Rating} size={'small'} />
</span>
{#if showMy && starCount > 0}
<span class="star-count">{starCount}</span>
{/if}
</button>
<!-- Other Emoji Reactions -->
{#each otherReactions as oreact (oreact[0])}
{@const emoji = oreact[1][0].emoji}
{@const data = oreact[1]}
<ReactionPresenter
{emoji}
selected={showMy && data.some((it) => socialIds.has(it.modifiedBy))}
socialIds={data.map((it) => it.modifiedBy)}
count={showMy ? data.length : 0}
on:click={() => removeReaction(oreact[1])}
/>
{/each}
<!-- Add Emoji Reaction Button -->
<ReactionPresenter icon={emojiPlugin.icon.EmojiAdd} iconSize="small" active={true} on:click={addEmojiReaction} />
</div>
</div>
<style lang="scss">
.rating-editor {
display: flex;
align-items: center;
gap: 0.15rem;
padding: 0.1rem;
background: transparent;
border-radius: 0;
border: none;
}
.reactions-container {
display: flex;
align-items: center;
gap: 0.25rem;
flex-wrap: nowrap;
}
.reaction-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
background: transparent;
border: 1px solid var(--button-secondary-BorderColor);
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.15s ease;
font-size: 0.9rem;
flex-shrink: 0;
min-height: 1.5rem;
gap: 0.25rem;
&:hover {
background: var(--button-secondary-hover-BackgroundColor);
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
}
.star-reaction {
@extend .reaction-btn;
}
.star-icon {
display: flex;
align-items: center;
justify-content: center;
}
.star-count {
font-size: 0.65rem;
font-weight: 600;
color: var(--global-primary-TextColor);
min-width: 0.75rem;
text-align: center;
}
.reaction-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
cursor: pointer;
transition: all 0.15s ease;
flex-shrink: 0;
&:hover {
transform: scale(1.15);
}
}
.reaction-emoji {
font-size: 0.9rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.reaction-remove {
position: absolute;
top: -0.35rem;
right: -0.35rem;
width: 0.9rem;
height: 0.9rem;
padding: 0;
background: #ff4444;
color: white;
border: none;
border-radius: 50%;
font-size: 0.5rem;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
&:hover {
transform: scale(1.15);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
&:active {
transform: scale(0.9);
}
}
.add-reaction-btn {
@extend .reaction-btn;
width: 1.5rem;
height: 1.5rem;
padding: 0;
border-radius: 50%;
}
</style>
@@ -0,0 +1,94 @@
<!-- Circular Level Display with Progress -->
<script lang="ts">
import { translateCB } from '@hcengineering/platform'
import ratingPlugin, { getLevelInfo } from '@hcengineering/rating'
export let rating: number = 0
export let showValues = false
$: levelInfo = getLevelInfo(rating)
let levelLabel: string | undefined = undefined
$: translateCB(ratingPlugin.string.Level, {}, undefined, (r) => (levelLabel = r))
</script>
{#if rating > 0}
<div class="flex-row-center">
{#if levelLabel}{levelLabel}{/if}
<div class="level-circle">
<svg class="progress-ring" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="60" cy="60" r="50" class="progress-ring-bg" />
<!-- Progress circle (bold overlay) -->
<circle
cx="60"
cy="60"
r="50"
class="progress-ring-fill"
style="stroke-dashoffset: {314.16 * (1 - levelInfo.progress / 100)}px"
/>
</svg>
<div class="level-content">
<span class="level-number">{levelInfo.level}</span>
</div>
</div>
{#if showValues}
<span class="text-sm caption-color">({Math.round(rating * 100)}/{Math.round(levelInfo.nextThreshold * 100)})</span
>
{/if}
</div>
{/if}
<style lang="scss">
.level-circle {
position: relative;
width: 2rem;
height: 2rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.progress-ring-bg {
fill: none;
stroke: var(--global-secondary-TextColor);
stroke-width: 1.5;
opacity: 0.15;
}
.progress-ring-fill {
fill: none;
stroke: #ffc107;
stroke-width: 3.5;
stroke-linecap: round;
transition: stroke-dashoffset 0.3s ease;
stroke-dasharray: 314.16;
&:hover {
filter: drop-shadow(0 0 0.2rem rgba(255, 193, 7, 0.7));
}
}
.level-content {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
}
.level-number {
font-size: 0.9rem;
font-weight: 700;
color: var(--global-primary-TextColor);
line-height: 1;
}
</style>
@@ -0,0 +1,87 @@
<script lang="ts">
import { getCurrentAccount, groupByArray, SortingOrder, type Class, type Doc, type Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import rating, { ReactionKind, type DocReaction } from '@hcengineering/rating'
import { NavGroup, Label } from '@hcengineering/ui'
import ScrollBox from '@hcengineering/ui/src/components/ScrollBox.svelte'
import NavigatorRating from './NavigatorRating.svelte'
const current = getCurrentAccount()
const query = createQuery()
let reactions: DocReaction[] = []
query.query(
rating.class.DocReaction,
{
createdBy: { $in: current.socialIds }
},
(results) => {
reactions = results
},
{ limit: 5000, sort: { modifiedOn: SortingOrder.Descending } }
)
function groupReactions (reactions: DocReaction[]): {
_class: Ref<Class<Doc>>
objects: {
_id: Ref<Doc>
reactions: DocReaction[]
}[]
}[] {
const starred = new Set(
reactions.filter((it) => it.reactionType === ReactionKind.Star && it.value === 1).map((it) => it.attachedTo)
)
const validReactions = reactions.filter(
(it) => it.attachedTo != null && it.attachedToClass != null && starred.has(it.attachedTo)
)
// I need to filter reactions without star's
const byBaseClass = groupByArray(validReactions, (it) => it.attachedToClass)
return Array.from(byBaseClass.entries()).map(([baseClass, reactions]) => {
const byDoc = Array.from(groupByArray(reactions, (it) => it.attachedTo)).map((it) => ({
_id: it[0],
reactions: it[1]
}))
return {
_class: baseClass,
objects: byDoc
}
})
}
$: groupped = groupReactions(reactions)
$: isEmpty = groupped.length === 0 || groupped.every((g) => g.objects.length === 0)
</script>
<div class="mt-2" />
{#if isEmpty}
<div class="p-4 text-center content-dark-color h-full w-full flex-grow" style:align-content="center">
<Label label={rating.string.RatingWidgetEmpty} />
</div>
{:else}
<ScrollBox vertical>
{#each groupped as group}
{@const classifier = getClient().getHierarchy().findClass(group._class)}
{#if classifier !== undefined}
<NavGroup
_id={group._class}
categoryName={'features'}
label={classifier.label}
type="nested"
isFold
empty={group.objects.length === 0}
noDivider
>
<div class="ml-2 mr-2">
<NavigatorRating _class={classifier} docs={group.objects} />
</div>
</NavGroup>
{/if}
{/each}
</ScrollBox>
{/if}
@@ -0,0 +1,84 @@
<!-- 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 { type PersonId } from '@hcengineering/core'
import { EmojiPresenter } from '@hcengineering/emoji-resources'
import { Icon, IconComponent, IconSize, tooltip } from '@hcengineering/ui'
import ReactionsTooltip from './ReactionsTooltip.svelte'
export let icon: IconComponent | undefined = undefined
export let iconSize: IconSize | undefined = undefined
export let emoji: string = ''
export let count: number | undefined = undefined
export let selected: boolean = false
export let active: boolean = false
export let socialIds: PersonId[] = []
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="reaction"
class:selected
class:active
on:click
use:tooltip={(count ?? 0) > 0 ? { component: ReactionsTooltip, props: { socialIds, emoji } } : undefined}
>
<div class="reaction__emoji" class:foreground={icon != null}>
{#if icon}
<Icon {icon} size={iconSize ?? 'small'} />
{:else}
<EmojiPresenter {emoji} />
{/if}
</div>
{#if count !== undefined && count > 0}
<div class="reaction__count">{count}</div>
{/if}
</div>
<style lang="scss">
.reaction {
display: flex;
height: 1.75rem;
padding: 0.375rem;
align-items: center;
gap: 0.25rem;
border-radius: 0.5rem;
background: var(--selector-BackgroundColor);
cursor: pointer;
&:hover,
&.active {
background: var(--global-ui-hover-BackgroundColor);
}
&.selected {
background: var(--global-accent-BackgroundColor);
}
}
.reaction__emoji {
color: var(--global-primary-TextColor);
font-size: 1rem;
font-weight: 400;
line-height: 1rem;
}
.reaction__count {
color: var(--global-primary-TextColor);
font-size: 0.75rem;
font-weight: 500;
}
</style>
@@ -0,0 +1,17 @@
<script lang="ts">
import { PersonId } from '@hcengineering/core'
import { ObjectPresenter } from '@hcengineering/view-resources'
import contact from '@hcengineering/contact'
import { getPersonRefByPersonIdStore } from '@hcengineering/contact-resources'
export let socialIds: PersonId[] = []
$: personRefByPersonIdStore = getPersonRefByPersonIdStore(socialIds)
$: persons = socialIds.map((si) => $personRefByPersonIdStore.get(si))
</script>
<div class="m-2 flex-col flex-gap-2">
{#each persons as person}
<ObjectPresenter objectId={person} _class={contact.class.Person} disabled />
{/each}
</div>
+29
View File
@@ -0,0 +1,29 @@
// 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 RatingEditor from './components/RatingEditor.svelte'
import RatingRing from './components/RatingRing.svelte'
import RatingWidget from './components/RatingWidget.svelte'
import DocReactionPresenter from './components/DocReactionPresenter.svelte'
import RatingActivities from './components/RatingActivities.svelte'
export default async (): Promise<Resources> => ({
component: {
RatingEditor,
RatingRing,
RatingWidget,
DocReactionPresenter,
RatingActivities
}
})
+21
View File
@@ -0,0 +1,21 @@
// Copyright © 2022 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 rating, { ratingId } from '@hcengineering/rating'
import { type AnyComponent } from '@hcengineering/ui/src/types'
export default mergeIds(ratingId, rating, {
component: {
RatingEditor: '' as AnyComponent
}
})
@@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
+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/rating",
"version": "0.7.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.7.19",
"@typescript-eslint/eslint-plugin": "^6.21.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.21.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/core": "^0.7.10",
"@hcengineering/platform": "^0.7.5",
"@hcengineering/ui": "^0.7.0"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"access": "public"
}
}
+131
View File
@@ -0,0 +1,131 @@
// 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 {
Class,
Domain,
Ref,
type AccountUuid,
type AttachedDoc,
type Blob,
type Doc,
type PersonId
} from '@hcengineering/core'
import { Asset, IntlString, plugin, Plugin } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
// [date, created, updated, deleted, messages, todos, hours]
export type DateCUD = [/* date */ number, /* created */ number, /* updated */ number, /* deleted */ number]
// Automatically calculated document rating.
export interface DocRating extends AttachedDoc {
// A current document rating
rating: number
updates: number // Total number of document updates
messages: number // Total number of child messages updates.
children: number // A child updates.
stars: number // A number of stars earned
reactions: number // A number of reactions earned
}
export enum ReactionKind {
Emoji = 0, // Just an emoji reaction, ❤️, 📌, 😊, 😕, 😠, 🤔
Star = 1, // A paid ⭐ 🌟, or paid negative star ✭ ✭, value = 1 => star visible for anyone and used for starred, value = 0, visible for anyone, but not shown in starred.
RateValue = 3 // A rating value from 0 to 10.
}
export interface DocReaction extends AttachedDoc {
// A reaction type identifier
reactionType: ReactionKind
value: number // +1, -1, 0..10 etc.
emoji?: string
image?: Ref<Blob>
}
export interface PersonRating extends Doc {
accountId: AccountUuid
// A current person rating
rating: number // Could be re calculated from values.
// Document authoring statistics
months: DateCUD[] // Hold per month authoring stats
// create/update/delete Hold per domain authoring stats
stats: Record<string, [number, number, number]>
socialIds: Record<PersonId, number>
// A special stats, stars on account and stars on documents, commentas authored by person
starsEarned: number // All author document stars.
reactionsEarned: number // All author document reactions
stars: number // Stars spend
reactions: number // Reactions created
rageOperations?: number
}
/**
* @public
*/
export const ratingId = 'rating' as Plugin
export const DOMAIN_RATING_REACTION = 'rating_reaction' as Domain
export const DOMAIN_PERSON_RATING = 'rating_person' as Domain
/**
* @public
*/
const ratingPlugin = plugin(ratingId, {
class: {
DocRating: '' as Ref<Class<DocRating>>,
PersonRating: '' as Ref<Class<PersonRating>>,
DocReaction: '' as Ref<Class<DocReaction>>
},
icon: {
Rating: '' as Asset,
StarYellow: '' as Asset,
StarRed: '' as Asset,
StarGreen: '' as Asset,
StarBlue: '' as Asset
},
string: {
Rating: '' as IntlString,
RatingWidget: '' as IntlString,
Level: '' as IntlString,
MonthOps: '' as IntlString,
MonthNoOps: '' as IntlString,
// Shown when the Rating widget has no items to display (empty state)
RatingWidgetEmpty: '' as IntlString,
AddStar: '' as IntlString,
AddStarAria: '' as IntlString
},
component: {
RatingRing: '' as AnyComponent,
RatingWidget: '' as AnyComponent,
DocReactionPresenter: '' as AnyComponent,
RatingActivities: '' as AnyComponent
},
ids: {
RatingWidget: '' as any
}
})
export default ratingPlugin
export * from './utils'
+56
View File
@@ -0,0 +1,56 @@
import type { PluginConfiguration } from '@hcengineering/core'
import type { PersonRating } from '.'
export interface LevelInfo {
level: number
progress: number // 0-100 percentage to next level
currentThreshold: number
nextThreshold: number
}
const BASE_MULTIPLIER = 1.5
export function ratingToLevel (rating: number): number {
// Convert experience rating to level (1-99) using logarithmic progression
// Base: level 1 at rating 0, multiplier 1.5 per level
// Level = 1 + log(rating + 1) / log(1.5)
const level = 1 + Math.log(Math.max(rating, 0) + 1) / Math.log(BASE_MULTIPLIER)
// Clamp to valid range 1-99
return Math.max(1, Math.min(99, Math.floor(level)))
}
export function getLevelInfo (rating: number): LevelInfo {
// Calculate current level
const currentLevel = ratingToLevel(rating)
// Calculate rating thresholds for current and next level
// Inverse formula: rating = multiplier^(level - 1) - 1
const currentThreshold = Math.round((Math.pow(BASE_MULTIPLIER, currentLevel - 1) - 1) * 100) / 100
const nextThreshold = Math.round((Math.pow(BASE_MULTIPLIER, currentLevel) - 1) * 100) / 100
// Calculate progress within current level (0-100)
const levelRange = nextThreshold - currentThreshold
const progressInLevel = rating - currentThreshold
const progress = levelRange > 0 ? Math.min(100, Math.max(0, (progressInLevel / levelRange) * 100)) : 0
return {
level: currentLevel,
progress,
currentThreshold,
nextThreshold
}
}
export function getRaiting (total: number, rating: PersonRating | undefined, list: PluginConfiguration[]): number {
if (rating?.stats == null || total === 0) return 0
let result = 0
for (const name of list) {
const st = rating.stats[name.pluginId]
if (st === undefined) {
continue
}
result += ((st[0] + st[1] + st[2]) / total) * 100
}
return Math.round(result * 100) / 100
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
}
+2 -1
View File
@@ -62,6 +62,7 @@
"@hcengineering/workbench-resources": "^0.7.0",
"@hcengineering/communication": "^0.7.0",
"@hcengineering/chat": "^0.7.0",
"@hcengineering/integration-client": "^0.7.0"
"@hcengineering/integration-client": "^0.7.0",
"@hcengineering/rating": "^0.7.0"
}
}
@@ -13,8 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { PluginConfiguration } from '@hcengineering/core'
import { pluginConfigurationStore, getClient } from '@hcengineering/presentation'
import { PluginConfiguration, systemAccountUuid } from '@hcengineering/core'
import { createQuery, getClient, pluginConfigurationStore, hasResource } from '@hcengineering/presentation'
import ratingPlugin, { getRaiting, type PersonRating } from '@hcengineering/rating'
import { Breadcrumb, Button, Header, Icon, IconInfo, Label, Scroller } from '@hcengineering/ui'
import setting from '../plugin'
@@ -25,6 +26,19 @@
enabled: value
})
}
const sysQuery = createQuery()
let sysRating: PersonRating | undefined
sysQuery.query(ratingPlugin.class.PersonRating, { accountId: systemAccountUuid }, (res) => {
sysRating = res[0]
})
$: totalVisible = getRaiting(
100,
sysRating,
$pluginConfigurationStore.list.filter((it) => it.enabled && it.hidden !== true && it.system !== true)
)
</script>
<div class="hulyComponent">
@@ -39,6 +53,7 @@
<div class="flex-row-center flex-wrap gap-around-4">
{#each $pluginConfigurationStore.list as config}
{#if config.hidden !== true && config.system !== true}
{@const pluginRating = getRaiting(totalVisible, sysRating, [config])}
<div class="cardBox flex-col clear-mins" class:enabled={config.enabled ?? true}>
<div class="flex-row-center">
<span class="mr-2">
@@ -46,6 +61,10 @@
</span>
<span class="fs-title">
<Label label={config.label} />
{#if hasResource(ratingPlugin.component.RatingRing)}
- ({pluginRating}%)
{/if}
</span>
</div>
{#if config.description}
@@ -18,20 +18,22 @@
import { getCurrentAccount, SocialIdType } from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login'
import { getResource } from '@hcengineering/platform'
import { AttributeEditor, MessageBox, getClient } from '@hcengineering/presentation'
import { AttributeEditor, createQuery, getClient, hasResource, MessageBox } from '@hcengineering/presentation'
import {
Breadcrumb,
Button,
Component,
createFocusManager,
EditBox,
FocusHandler,
Header,
createFocusManager,
showPopup,
navigate,
Scroller
Scroller,
showPopup
} from '@hcengineering/ui'
import { logIn, logOut } from '@hcengineering/workbench-resources'
import rating, { type PersonRating } from '@hcengineering/rating'
import setting from '../plugin'
import SocialIdsEditor from './socialIds/SocialIdsEditor.svelte'
@@ -39,6 +41,16 @@
const account = getCurrentAccount()
const email = account.fullSocialIds.find((si) => si.type === SocialIdType.EMAIL)?.value ?? ''
const levelQuery = createQuery()
let personRating: PersonRating | undefined
levelQuery.query(rating.class.PersonRating, { accountId: account.uuid }, (res) => {
personRating = res[0]
})
$: console.log('SYS', personRating)
let firstName = ''
let lastName = ''
let initialized = false
@@ -102,7 +114,7 @@
<div class="ac-body p-10 flex-col max-w-240 content">
{#if $myEmployeeStore}
<div class="flex flex-grow w-full">
<div class="mr-8">
<div class="mr-8 flex-col items-center">
<EditableAvatar
person={$myEmployeeStore}
{email}
@@ -111,6 +123,14 @@
bind:this={avatarEditor}
on:done={onAvatarDone}
/>
{#if hasResource(rating.component.RatingRing)}
<div class="flex-row-center">
<Component
is={rating.component.RatingRing}
props={{ rating: personRating?.rating ?? 0, showValues: true }}
/>
</div>
{/if}
</div>
<div class="flex-grow flex-col">
<EditBox
@@ -149,7 +169,15 @@
</div>
{/if}
<div class="separator" />
<SocialIdsEditor />
{#if hasResource(rating.component.RatingRing)}
{#if personRating != null}
<div class="flex-row-center mt-2">
<Component is={rating.component.RatingActivities} props={{ rating: personRating }} />
</div>
{/if}
<div class="separator" />
{/if}
<SocialIdsEditor rating={personRating} />
<div class="footer">
<Button
icon={setting.icon.Signout}
@@ -13,7 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import contact, { SocialIdentity, SocialIdentityProvider, SocialIdentityRef } from '@hcengineering/contact'
import { SocialIdentityPresenter } from '@hcengineering/contact-resources'
import {
getCurrentAccount,
loginSocialTypes,
@@ -21,19 +22,20 @@
setCurrentAccount,
SocialId
} from '@hcengineering/core'
import { Button, getPlatformColorDef, Label, PaletteColorIndexes, showPopup, themeStore } from '@hcengineering/ui'
import contact, { SocialIdentity, SocialIdentityProvider, SocialIdentityRef } from '@hcengineering/contact'
import { SocialIdentityPresenter } from '@hcengineering/contact-resources'
import { getClient, MessageBox } from '@hcengineering/presentation'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient, MessageBox } from '@hcengineering/presentation'
import { getPlatformColorDef, Label, PaletteColorIndexes, showPopup, themeStore } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { Analytics } from '@hcengineering/analytics'
import setting from '../../plugin'
import { getAccountClient } from '../../utils'
import { Analytics } from '@hcengineering/analytics'
export let socialId: SocialId
export let socialIdProvider: SocialIdentityProvider
export let canRelease: boolean = false
export let socialIdRating: number | undefined = undefined
export let socialIdRatingTotal: number | undefined = undefined
const client = getClient()
const accountClient = getAccountClient()
@@ -116,6 +118,12 @@
<div class="flex-row-center flex-gap-2 root">
<SocialIdentityPresenter value={socialIdentity} {socialIdProvider} />
{#if socialIdRating != null && socialIdRatingTotal != null && socialIdRatingTotal > 0}
<div class="on-hover">
{Math.round((socialIdRating / socialIdRatingTotal) * 100)}%
</div>
{/if}
<div class="flex-grow" />
{#if isLogin}
@@ -13,14 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import { getCurrentAccount, loginSocialTypes, notEmpty, SocialIdType } from '@hcengineering/core'
import { FocusHandler, createFocusManager, showPopup, Label, Button, Menu, Action, Scroller } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import contact from '@hcengineering/contact'
import { getCurrentAccount, loginSocialTypes, notEmpty, SocialIdType } from '@hcengineering/core'
import { getClient, hasResource } from '@hcengineering/presentation'
import { Action, Button, createFocusManager, FocusHandler, Label, Menu, Scroller, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import SocialIdRow from './SocialIdRow.svelte'
import type { PersonRating } from '@hcengineering/rating'
import ratingPlugin from '@hcengineering/rating'
import setting from '../../plugin'
import SocialIdRow from './SocialIdRow.svelte'
export let rating: PersonRating | undefined
let account = getCurrentAccount()
const manager = createFocusManager()
@@ -59,6 +63,9 @@
function handleAccountUpdated (): void {
account = getCurrentAccount()
}
$: socialIdRatingTotal =
rating != null ? Object.values(rating?.socialIds ?? {}).reduce((sum, val) => sum + (val ?? 0), 0) : 0
</script>
<FocusHandler {manager} />
@@ -80,8 +87,19 @@
? !onlyLogin
: true}
{#if socialIdProvider != null && socialId.type !== SocialIdType.HULY}
{@const socialIdRating =
rating != null && hasResource(ratingPlugin.component.RatingRing)
? rating.socialIds?.[socialId?._id]
: undefined}
<div class="item">
<SocialIdRow {socialId} {socialIdProvider} {canRelease} on:released={handleAccountUpdated} />
<SocialIdRow
{socialId}
{socialIdProvider}
{canRelease}
on:released={handleAccountUpdated}
{socialIdRating}
{socialIdRatingTotal}
/>
</div>
{/if}
{/each}
+16 -8
View File
@@ -1,18 +1,26 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { getClient as getAccountClientRaw, type AccountClient } from '@hcengineering/account-client'
import contact, { getFirstName, getLastName } from '@hcengineering/contact'
import { employeeByPersonIdStore } from '@hcengineering/contact-resources'
import { type IntegrationKind, type Class, type Doc, type Hierarchy, type Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import login from '@hcengineering/login'
import presentation, { getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { type TemplateDataProvider } from '@hcengineering/templates'
import { getClient as getAccountClientRaw, type AccountClient } from '@hcengineering/account-client'
import { get } from 'svelte/store'
import {
type Class,
type Doc,
type Hierarchy,
type IntegrationKind,
type PluginConfiguration,
type Ref
} from '@hcengineering/core'
import {
getIntegrationClient as getIntegrationClientRaw,
type IntegrationClient
} from '@hcengineering/integration-client'
import login from '@hcengineering/login'
import { getMetadata } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import type { PersonRating } from '@hcengineering/rating'
import setting from '@hcengineering/setting'
import { type TemplateDataProvider } from '@hcengineering/templates'
import { get } from 'svelte/store'
function isEditable (hierarchy: Hierarchy, p: Class<Doc>): boolean {
let ancestors = [p._id]

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