UBERF-4248: Task type (#4042)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-12-14 23:26:02 +07:00 committed by GitHub
parent 58b841518a
commit 19d6764250
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
196 changed files with 4690 additions and 2524 deletions

2
.gitignore vendored
View File

@ -86,4 +86,4 @@ dev/tool/report*.csv
tests/db_dump
.build
.format
tools/apm/apm.js
tools/apm/apm.js

View File

@ -37,7 +37,8 @@ const object: AttachedData<Issue> = {
remainingTime: 0,
estimation: 0,
reports: 0,
childInfo: []
childInfo: [],
kind: tracker.taskTypes.Issue
}
export interface IssueOptions {
@ -100,7 +101,8 @@ async function genIssue (client: TxOperations, statuses: Ref<IssueStatus>[]): Pr
estimation: object.estimation,
reports: 0,
relations: [],
childInfo: []
childInfo: [],
kind: tracker.taskTypes.Issue
}
await client.addCollection(
tracker.class.Issue,

View File

@ -15,7 +15,7 @@ import core, {
import { MinioService } from '@hcengineering/minio'
import recruit from '@hcengineering/model-recruit'
import { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task, { ProjectType, genRanks } from '@hcengineering/task'
import task, { ProjectType, TaskType, genRanks } from '@hcengineering/task'
import faker from 'faker'
import jpeg, { BufferRet } from 'jpeg-js'
import { AttachmentOptions, addAttachments } from './attachments'
@ -97,13 +97,16 @@ async function genVacansyApplicants (
name: faker.name.title(),
description: faker.lorem.sentences(2),
shortDescription: faker.lorem.sentences(1),
category: recruit.category.VacancyTypeCategories,
descriptor: recruit.descriptors.VacancyType,
private: false,
members: [],
archived: false,
tasks: [],
// TODO: Fix me.
statuses: states.map((s) => {
return { _id: s }
})
return { _id: s, taskType: '' as Ref<TaskType> }
}),
targetClass: recruit.class.Vacancy
}
await ctx.with('update', {}, (ctx) =>
@ -180,7 +183,8 @@ async function genApplicant (
status: faker.random.arrayElement(states),
rank,
startDate: null,
dueDate: null
dueDate: null,
kind: recruit.taskTypes.Applicant
}
// Update or create candidate

View File

@ -516,16 +516,26 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(
task.class.ProjectTypeCategory,
task.class.ProjectTypeDescriptor,
core.space.Model,
{
name: board.string.Boards,
name: board.string.BoardApplication,
description: board.string.ManageBoardStatuses,
icon: board.component.TemplatesIcon,
attachedToClass: board.class.Board,
statusClass: core.class.Status,
statusCategories: [task.statusCategory.Active, task.statusCategory.Won, task.statusCategory.Lost]
baseClass: board.class.Board
},
board.category.BoardType
board.descriptors.BoardType
)
builder.createDoc(
task.class.TaskTypeDescriptor,
core.space.Model,
{
baseClass: board.class.Card,
allowCreate: true,
description: board.string.Card,
icon: board.icon.Card,
name: board.string.Card
},
board.descriptors.Card
)
}

View File

@ -13,19 +13,21 @@
// limitations under the License.
//
import { boardId } from '@hcengineering/board'
import { type Ref, TxOperations } from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate
createOrUpdate,
tryMigrate
} from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { createProjectType, createSequence } from '@hcengineering/model-task'
import tags from '@hcengineering/model-tags'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task'
import tags from '@hcengineering/tags'
import task, { type ProjectType } from '@hcengineering/task'
import board from './plugin'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import board from './plugin'
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
@ -54,27 +56,51 @@ async function createDefaultProjectType (tx: TxOperations): Promise<Ref<ProjectT
tx,
{
name: 'Default board',
category: board.category.BoardType,
description: ''
descriptor: board.descriptors.BoardType,
description: '',
tasks: []
},
[
{
color: PaletteColorIndexes.Blueberry,
name: 'To do',
category: task.statusCategory.Active,
ofAttribute: board.attribute.State
},
{
color: PaletteColorIndexes.Arctic,
name: 'Done',
category: task.statusCategory.Active,
ofAttribute: board.attribute.State
},
{
color: PaletteColorIndexes.Grass,
name: 'Completed',
category: board.statusCategory.Completed,
ofAttribute: board.attribute.State
_id: board.taskType.Card,
descriptor: board.descriptors.Card,
name: 'Card',
ofClass: board.class.Card,
targetClass: board.class.Card,
statusClass: core.class.Status,
kind: 'task',
factory: [
{
color: PaletteColorIndexes.Coin,
name: 'Unstarted',
category: task.statusCategory.UnStarted,
ofAttribute: board.attribute.State
},
{
color: PaletteColorIndexes.Blueberry,
name: 'To do',
category: task.statusCategory.Active,
ofAttribute: board.attribute.State
},
{
color: PaletteColorIndexes.Arctic,
name: 'Done',
category: task.statusCategory.Active,
ofAttribute: board.attribute.State
},
{
color: PaletteColorIndexes.Grass,
name: 'Completed',
category: board.statusCategory.Completed,
ofAttribute: board.attribute.State
}
],
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
]
}
],
board.template.DefaultBoard
@ -100,7 +126,44 @@ async function createDefaults (tx: TxOperations): Promise<void> {
}
export const boardOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, boardId, [
{
state: 'fix-category-descriptors',
func: async (client) => {
await client.update(
DOMAIN_SPACE,
{ _class: task.class.ProjectType, category: 'board:category:BoardType' },
{
$set: { descriptor: board.descriptors.BoardType },
$unset: { category: 1 }
}
)
}
},
{
state: 'fixTaskStatus',
func: async (client): Promise<void> => {
await fixTaskTypes(client, board.descriptors.BoardType, async () => [
{
name: 'Card',
descriptor: board.descriptors.Card,
ofClass: board.class.Card,
targetClass: board.class.Card,
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
statusClass: core.class.Status,
kind: 'task'
}
])
}
}
])
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const ops = new TxOperations(client, core.account.System)
await createDefaults(ops)

View File

@ -18,7 +18,7 @@ import { type Board, boardId } from '@hcengineering/board'
import board from '@hcengineering/board-resources/src/plugin'
import type { Ref } from '@hcengineering/core'
import { type IntlString, mergeIds } from '@hcengineering/platform'
import { type ProjectType, type Sequence } from '@hcengineering/task'
import { type TaskTypeDescriptor, type ProjectType, type Sequence } from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ViewAction, type Viewlet, type ViewletDescriptor } from '@hcengineering/view'
@ -67,5 +67,8 @@ export default mergeIds(boardId, board, {
},
actionImpl: {
ConvertToCard: '' as ViewAction
},
descriptors: {
Card: '' as Ref<TaskTypeDescriptor>
}
})

View File

@ -54,7 +54,6 @@ export default mergeIds(contactId, contact, {
ContactArrayEditor: '' as AnyComponent,
EmployeeEditor: '' as AnyComponent,
CreateEmployee: '' as AnyComponent,
AccountArrayEditor: '' as AnyComponent,
ChannelFilter: '' as AnyComponent,
MergePersons: '' as AnyComponent,
ChannelPanel: '' as AnyComponent,

View File

@ -689,16 +689,26 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(
task.class.ProjectTypeCategory,
task.class.ProjectTypeDescriptor,
core.space.Model,
{
name: lead.string.Funnels,
name: lead.string.LeadApplication,
description: lead.string.ManageFunnelStatuses,
icon: lead.component.TemplatesIcon,
attachedToClass: lead.class.Funnel,
statusClass: core.class.Status,
statusCategories: [task.statusCategory.Active, task.statusCategory.Won, task.statusCategory.Lost]
baseClass: lead.class.Funnel
},
lead.category.FunnelTypeCategory
lead.descriptors.FunnelType
)
builder.createDoc(
task.class.TaskTypeDescriptor,
core.space.Model,
{
baseClass: lead.class.Lead,
allowCreate: true,
description: lead.string.Lead,
icon: lead.icon.Lead,
name: lead.string.Lead
},
lead.descriptors.Lead
)
}

View File

@ -16,15 +16,14 @@
import { TxOperations } from '@hcengineering/core'
import { leadId } from '@hcengineering/lead'
import {
tryMigrate,
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
tryUpgrade
type MigrationUpgradeClient
} from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { createProjectType, createSequence } from '@hcengineering/model-task'
import tracker from '@hcengineering/model-tracker'
import task from '@hcengineering/task'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import task, { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import lead from './plugin'
@ -37,42 +36,66 @@ async function createSpace (tx: TxOperations): Promise<void> {
tx,
{
name: 'Default funnel',
category: lead.category.FunnelTypeCategory,
description: ''
descriptor: lead.descriptors.FunnelType,
description: '',
tasks: []
},
[
{
color: PaletteColorIndexes.Coin,
name: 'Incoming',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Arctic,
name: 'Negotation',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Watermelon,
name: 'Offer preparing',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Orange,
name: 'Make a decision',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Ocean,
name: 'Contract conclusion',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{ name: 'Won', ofAttribute: lead.attribute.State, category: task.statusCategory.Won },
{ name: 'Lost', ofAttribute: lead.attribute.State, category: task.statusCategory.Lost }
_id: lead.taskType.Lead,
name: 'Lead',
descriptor: lead.descriptors.Lead,
ofClass: lead.class.Lead,
targetClass: lead.class.Lead,
statusClass: core.class.Status,
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
kind: 'task',
factory: [
{
color: PaletteColorIndexes.Coin,
name: 'Backlog',
ofAttribute: lead.attribute.State,
category: task.statusCategory.UnStarted
},
{
color: PaletteColorIndexes.Coin,
name: 'Incoming',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Arctic,
name: 'Negotation',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Watermelon,
name: 'Offer preparing',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Orange,
name: 'Make a decision',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Ocean,
name: 'Contract conclusion',
ofAttribute: lead.attribute.State,
category: task.statusCategory.Active
},
{ name: 'Won', ofAttribute: lead.attribute.State, category: task.statusCategory.Won },
{ name: 'Lost', ofAttribute: lead.attribute.State, category: task.statusCategory.Lost }
]
}
],
lead.template.DefaultFunnel
)
@ -98,24 +121,48 @@ async function createDefaults (tx: TxOperations): Promise<void> {
}
export const leadOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, leadId, [
{
state: 'fix-category-descriptors',
func: async (client) => {
await client.update(
DOMAIN_SPACE,
{ _class: task.class.ProjectType, category: 'lead:category:FunnelTypeCategory' },
{
$set: { descriptor: lead.descriptors.FunnelType },
$unset: { category: 1 }
}
)
}
},
{
state: 'fixTaskStatus',
func: async (client): Promise<void> => {
await fixTaskTypes(client, lead.descriptors.FunnelType, async () => [
{
name: 'Lead',
descriptor: lead.descriptors.Lead,
ofClass: lead.class.Lead,
targetClass: lead.class.Lead,
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
statusClass: core.class.Status,
kind: 'task'
}
])
}
}
])
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const ops = new TxOperations(client, core.account.System)
await createDefaults(ops)
await tryUpgrade(client, leadId, [
{
state: 'related-targets',
func: async (client): Promise<void> => {
const ops = new TxOperations(client, core.account.ConfigUser)
await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, {
rule: {
kind: 'classRule',
ofClass: lead.class.Lead
}
})
}
}
])
await tryUpgrade(client, leadId, [])
}
}

View File

@ -21,7 +21,7 @@ import lead from '@hcengineering/lead-resources/src/plugin'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import type { IntlString } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { type ProjectType } from '@hcengineering/task'
import { type TaskTypeDescriptor, type ProjectType } from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type Viewlet } from '@hcengineering/view'
@ -72,5 +72,8 @@ export default mergeIds(leadId, lead, {
LeadCreateNotification: '' as Ref<NotificationType>,
AssigneeNotification: '' as Ref<NotificationType>,
LeadChatMessageViewlet: '' as Ref<ChatMessageViewlet>
},
descriptors: {
Lead: '' as Ref<TaskTypeDescriptor>
}
})

View File

@ -276,9 +276,9 @@ export function createModel (builder: Builder): void {
label: notification.string.Notifications,
icon: notification.icon.Notifications,
component: notification.component.NotificationSettings,
group: 'settings',
group: 'settings-account',
secured: false,
order: 2500
order: 1500
},
notification.ids.NotificationSettings
)

View File

@ -316,7 +316,7 @@ export function createModel (builder: Builder): void {
label: recruit.string.Applications,
createLabel: recruit.string.ApplicationCreateLabel,
createComponent: recruit.component.CreateApplication,
category: recruit.category.VacancyTypeCategories,
descriptor: recruit.descriptors.VacancyType,
descriptors: [
view.viewlet.Table,
view.viewlet.List,
@ -1750,17 +1750,28 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
task.class.ProjectTypeCategory,
task.class.ProjectTypeDescriptor,
core.space.Model,
{
name: recruit.string.Vacancies,
name: recruit.string.RecruitApplication,
description: recruit.string.ManageVacancyStatuses,
icon: recruit.component.TemplatesIcon,
editor: recruit.component.VacancyTemplateEditor,
attachedToClass: recruit.class.Vacancy,
statusClass: core.class.Status,
statusCategories: [task.statusCategory.Active, task.statusCategory.Won, task.statusCategory.Lost]
baseClass: recruit.class.Vacancy
},
recruit.category.VacancyTypeCategories
recruit.descriptors.VacancyType
)
builder.createDoc(
task.class.TaskTypeDescriptor,
core.space.Model,
{
baseClass: recruit.class.Applicant,
allowCreate: true,
description: recruit.string.Application,
icon: recruit.icon.Application,
name: recruit.string.Application
},
recruit.descriptors.Application
)
}

View File

@ -14,70 +14,67 @@
//
import { getCategories } from '@anticrm/skillset'
import core, { type Ref, TxOperations } from '@hcengineering/core'
import core, { TxOperations, type Ref } from '@hcengineering/core'
import {
createOrUpdate,
tryMigrate,
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate,
tryUpgrade
type MigrationUpgradeClient
} from '@hcengineering/model'
import tags, { type TagCategory } from '@hcengineering/model-tags'
import { createProjectType, createSequence } from '@hcengineering/model-task'
import tracker from '@hcengineering/model-tracker'
import { createProjectType, createSequence, fixTaskTypes } from '@hcengineering/model-task'
import { recruitId } from '@hcengineering/recruit'
import task, { type ProjectType } from '@hcengineering/task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import recruit from './plugin'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
export const recruitOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, recruitId, [
{
state: 'fix-category-descriptors',
func: async (client) => {
await client.update(
DOMAIN_SPACE,
{ _class: task.class.ProjectType, category: 'recruit:category:VacancyTypeCategories' },
{
$set: { descriptor: recruit.descriptors.VacancyType },
$unset: { category: 1 }
}
)
}
},
{
state: 'fixTaskStatus',
func: async (client): Promise<void> => {
await fixTaskTypes(client, recruit.descriptors.VacancyType, async () => [
{
name: 'Applicant',
descriptor: recruit.descriptors.Application,
ofClass: recruit.class.Applicant,
targetClass: recruit.class.Applicant,
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
statusClass: core.class.Status,
kind: 'task'
}
])
}
}
])
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)
await tryUpgrade(client, recruitId, [
{
state: 'related-targets',
func: async (client): Promise<void> => {
const ops = new TxOperations(client, core.account.ConfigUser)
await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, {
rule: {
kind: 'classRule',
ofClass: recruit.class.Vacancy
}
})
await ops.createDoc(tracker.class.RelatedIssueTarget, core.space.Configuration, {
rule: {
kind: 'classRule',
ofClass: recruit.class.Applicant
}
})
}
},
{
state: 'wrong-categories',
func: async (client): Promise<void> => {
const ops = new TxOperations(client, core.account.System)
while (true) {
const docs = await ops.findAll(
tags.class.TagElement,
{
targetClass: recruit.mixin.Candidate,
category: { $in: [tracker.category.Other, 'document:category:Other' as Ref<TagCategory>] }
},
{ limit: 1000 }
)
for (const d of docs) {
await ops.update(d, { category: recruit.category.Other })
}
if (docs.length === 0) {
break
}
}
}
},
{
state: 'remove-members',
func: async (client): Promise<void> => {
@ -137,36 +134,60 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Proje
tx,
{
name: 'Default vacancy',
category: recruit.category.VacancyTypeCategories,
description: ''
descriptor: recruit.descriptors.VacancyType,
description: '',
tasks: []
},
[
{
color: PaletteColorIndexes.Coin,
name: 'HR Interview',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Cerulean,
name: 'Technical Interview',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Waterway,
name: 'Test task',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Grass,
name: 'Offer',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{ name: 'Won', ofAttribute: recruit.attribute.State, category: task.statusCategory.Won },
{ name: 'Lost', ofAttribute: recruit.attribute.State, category: task.statusCategory.Lost }
_id: recruit.taskTypes.Applicant,
name: 'Applicant',
descriptor: recruit.descriptors.Application,
ofClass: recruit.class.Applicant,
targetClass: recruit.class.Applicant,
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
statusClass: core.class.Status,
kind: 'task',
factory: [
{
color: PaletteColorIndexes.Coin,
name: 'Backlog',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.UnStarted
},
{
color: PaletteColorIndexes.Coin,
name: 'HR Interview',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Cerulean,
name: 'Technical Interview',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Waterway,
name: 'Test task',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{
color: PaletteColorIndexes.Grass,
name: 'Offer',
ofAttribute: recruit.attribute.State,
category: task.statusCategory.Active
},
{ name: 'Won', ofAttribute: recruit.attribute.State, category: task.statusCategory.Won },
{ name: 'Lost', ofAttribute: recruit.attribute.State, category: task.statusCategory.Lost }
]
}
],
recruit.template.DefaultVacancy
)

View File

@ -19,7 +19,7 @@ import type { IntlString, Resource, Status } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { recruitId } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit-resources/src/plugin'
import { type ProjectType } from '@hcengineering/task'
import { type TaskTypeDescriptor, type ProjectType } from '@hcengineering/task'
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
import type { Action, ActionCategory, ViewAction, ViewQueryAction, Viewlet } from '@hcengineering/view'
import { type DocUpdateMessageViewlet } from '@hcengineering/activity'
@ -138,5 +138,8 @@ export default mergeIds(recruitId, recruit, {
TableReview: '' as Ref<Viewlet>,
TableVacancyList: '' as Ref<Viewlet>,
ApplicantDashboard: '' as Ref<Viewlet>
},
descriptors: {
Application: '' as Ref<TaskTypeDescriptor>
}
})

View File

@ -15,23 +15,22 @@
import activity from '@hcengineering/activity'
import contact from '@hcengineering/contact'
import { type Account, type Domain, DOMAIN_MODEL, type Ref } from '@hcengineering/core'
import { type Builder, Mixin, Model } from '@hcengineering/model'
import { DOMAIN_MODEL, type Account, type Domain, type Ref } from '@hcengineering/core'
import { Mixin, Model, type Builder } from '@hcengineering/model'
import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core'
import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
import type { Asset, IntlString } from '@hcengineering/platform'
import {
settingId,
type Editable,
type Handler,
type Integration,
type IntegrationType,
type InviteSettings,
settingId,
type SettingsCategory,
type UserMixin
} from '@hcengineering/setting'
import task from '@hcengineering/task'
import templates from '@hcengineering/templates'
import setting from './plugin'
@ -39,8 +38,8 @@ import workbench from '@hcengineering/model-workbench'
import { type AnyComponent } from '@hcengineering/ui/src/types'
export { settingId } from '@hcengineering/setting'
export { default } from './plugin'
export { settingOperation } from './migration'
export { default } from './plugin'
export const DOMAIN_SETTING = 'setting' as Domain
@ -153,6 +152,9 @@ export function createModel (builder: Builder): void {
label: setting.string.WorkspaceSetting,
icon: setting.icon.Setting,
component: setting.component.WorkspaceSettings,
extraComponents: {
navigation: setting.component.WorkspaceSettings
},
group: 'settings',
secured: false,
order: 2000
@ -167,9 +169,9 @@ export function createModel (builder: Builder): void {
label: setting.string.Integrations,
icon: setting.icon.Integrations,
component: setting.component.Integrations,
group: 'settings',
group: 'settings-account',
secured: false,
order: 3000
order: 1500
},
setting.ids.Integrations
)
@ -200,21 +202,7 @@ export function createModel (builder: Builder): void {
setting.ids.Configure
)
builder.createDoc(
setting.class.WorkspaceSettingCategory,
core.space.Model,
{
name: 'statuses',
label: setting.string.ManageProjects,
icon: task.icon.ManageTemplates,
component: setting.component.ManageProjects,
group: 'settings-editor',
secured: false,
order: 4000
},
setting.ids.ManageProjects
)
builder.createDoc(
setting.class.WorkspaceSettingCategory,
setting.class.SettingsCategory,
core.space.Model,
{
name: 'classes',
@ -237,7 +225,8 @@ export function createModel (builder: Builder): void {
component: setting.component.EnumSetting,
group: 'settings-editor',
secured: false,
order: 4600
order: 4600,
expandable: true
},
setting.ids.EnumSetting
)

View File

@ -42,6 +42,7 @@
"@hcengineering/task-resources": "^0.6.0",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9"
"@hcengineering/setting": "^0.6.11",
"@hcengineering/notification": "^0.6.16"
}
}

View File

@ -16,18 +16,25 @@
import type { Employee, Person } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import {
type Class,
ClassifierKind,
DOMAIN_MODEL,
DOMAIN_STATUS,
DOMAIN_TX,
IndexKind,
generateId,
type Class,
type Data,
type Doc,
type Domain,
IndexKind,
type Ref,
type Status,
type StatusCategory,
type Timestamp
type Timestamp,
type TxCreateDoc,
type TxMixin
} from '@hcengineering/core'
import {
type Builder,
ArrOf,
Collection,
Hidden,
Index,
@ -37,34 +44,50 @@ import {
TypeBoolean,
TypeDate,
TypeMarkup,
TypeRecord,
TypeRef,
TypeString,
UX
UX,
type Builder,
type MigrationClient
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import { type IntlString } from '@hcengineering/platform'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import chunter from '@hcengineering/model-chunter'
import core, { DOMAIN_SPACE, TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import view, {
classPresenter,
createAction,
template,
actionTemplates as viewTemplates
} from '@hcengineering/model-view'
import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import tags from '@hcengineering/tags'
import {
calculateStatuses,
findStatusAttr,
type KanbanCard,
type Project,
type ProjectStatus,
type ProjectType,
type ProjectTypeCategory,
type ProjectTypeClass,
type ProjectTypeDescriptor,
type Sequence,
type Task,
type TaskType,
type TaskTypeClass,
type TaskTypeDescriptor,
type TaskTypeKind,
type TodoItem
} from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import type { AnyComponent } from '@hcengineering/ui'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
import { type ViewAction } from '@hcengineering/view'
import chunter from '@hcengineering/model-chunter'
import task from './plugin'
export { taskId } from '@hcengineering/task'
export { createProjectType, createSequence, taskOperation } from './migration'
export { createProjectType, taskId } from '@hcengineering/task'
export { createSequence, taskOperation } from './migration'
export { default } from './plugin'
export const DOMAIN_TASK = 'task' as Domain
@ -80,6 +103,10 @@ export class TTask extends TAttachedDoc implements Task {
@Index(IndexKind.Indexed)
status!: Ref<Status>
@Prop(TypeRef(task.class.TaskType), task.string.TaskType)
@Index(IndexKind.Indexed)
kind!: Ref<TaskType>
@Prop(TypeString(), task.string.TaskNumber)
@Index(IndexKind.FullText)
@Hidden()
@ -132,27 +159,91 @@ export class TKanbanCard extends TClass implements KanbanCard {
card!: AnyComponent
}
@Model(task.class.TaskTypeDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TTaskTypeDescriptor extends TDoc implements TaskTypeDescriptor {
name!: IntlString
description!: IntlString
icon!: Asset
baseClass!: Ref<Class<Task>>
// If specified, will allow to be created by users, system type overwize
allowCreate!: boolean
}
@Mixin(task.mixin.TaskTypeClass, core.class.Class)
export class TTaskTypeClass extends TClass implements TaskTypeClass {
taskType!: Ref<TaskType>
projectType!: Ref<ProjectType>
}
@Mixin(task.mixin.ProjectTypeClass, core.class.Class)
export class TProjectTypeClass extends TClass implements ProjectTypeClass {
projectType!: Ref<ProjectType>
}
@Model(task.class.Project, core.class.Space)
export class TProject extends TSpace implements Project {
type!: Ref<ProjectType>
@Prop(TypeRef(task.class.ProjectType), task.string.ProjectType)
type!: Ref<ProjectType>
}
@Model(task.class.ProjectType, core.class.Space)
export class TProjectType extends TSpace implements ProjectType {
statuses!: ProjectStatus[]
shortDescription?: string
category!: Ref<ProjectTypeCategory>
@Prop(TypeRef(task.class.ProjectTypeDescriptor), getEmbeddedLabel('Descriptor'))
descriptor!: Ref<ProjectTypeDescriptor>
@Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Tasks'))
tasks!: Ref<TaskType>[]
@Prop(ArrOf(TypeRecord()), getEmbeddedLabel('Project statuses'))
statuses!: ProjectStatus[]
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class'))
targetClass!: Ref<Class<Project>>
}
@Model(task.class.ProjectTypeCategory, core.class.Doc, DOMAIN_MODEL)
export class TProjectTypeCategory extends TDoc implements ProjectTypeCategory {
@Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK)
export class TTaskType extends TDoc implements TaskType {
@Prop(TypeString(), getEmbeddedLabel('Name'))
name!: string
@Prop(TypeRef(task.class.TaskTypeDescriptor), getEmbeddedLabel('Descriptor'))
descriptor!: Ref<TaskTypeDescriptor>
@Prop(TypeRef(task.class.ProjectType), getEmbeddedLabel('Task class'))
parent!: Ref<ProjectType> // Base class for task
@Prop(TypeString(), getEmbeddedLabel('Kind'))
kind!: TaskTypeKind
@Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Parent'))
allowedAsChildOf!: Ref<TaskType>[] // In case of specified, task type is for sub-tasks
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task class'))
ofClass!: Ref<Class<Task>> // Base class for task
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task target class'))
targetClass!: Ref<Class<Task>> // Class or Mixin mixin to hold all user defined attributes.
@Prop(ArrOf(TypeRef(core.class.Status)), getEmbeddedLabel('Task statuses'))
statuses!: Ref<Status>[]
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task status class'))
statusClass!: Ref<Class<Status>>
@Prop(TypeRef(core.class.StatusCategory), getEmbeddedLabel('Task status categories'))
statusCategories!: Ref<StatusCategory>[]
}
@Model(task.class.ProjectTypeDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TProjectTypeDescriptor extends TDoc implements ProjectTypeDescriptor {
name!: IntlString
description!: IntlString
icon!: AnyComponent
editor?: AnyComponent
attachedToClass!: Ref<Class<Project>>
statusClass!: Ref<Class<Status>>
statusCategories!: Ref<StatusCategory>[]
baseClass!: Ref<Class<Task>>
}
@Model(task.class.Sequence, core.class.Doc, DOMAIN_KANBAN)
@ -218,7 +309,19 @@ export const actionTemplates = template({
})
export function createModel (builder: Builder): void {
builder.createModel(TKanbanCard, TSequence, TTask, TTodoItem, TProject, TProjectType, TProjectTypeCategory)
builder.createModel(
TKanbanCard,
TSequence,
TTask,
TTodoItem,
TProject,
TProjectType,
TTaskType,
TProjectTypeDescriptor,
TTaskTypeDescriptor,
TTaskTypeClass,
TProjectTypeClass
)
builder.createDoc(
view.class.ViewletDescriptor,
@ -243,6 +346,10 @@ export function createModel (builder: Builder): void {
editor: task.component.TaskHeader
})
builder.mixin(task.class.ProjectType, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open]
})
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
@ -312,6 +419,16 @@ export function createModel (builder: Builder): void {
presenter: task.component.TodoItemPresenter
})
builder.mixin(task.class.TaskType, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.TaskTypePresenter
})
builder.mixin(task.class.ProjectType, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.ProjectTypePresenter
})
classPresenter(builder, task.class.TaskType, task.component.TaskTypePresenter, task.component.TaskTypePresenter)
createAction(builder, {
label: task.string.MarkAsDone,
icon: task.icon.TodoCheck,
@ -370,7 +487,21 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
ofAttribute: task.attribute.State,
label: core.string.Status,
label: task.string.StateBacklog,
icon: task.icon.TaskState,
color: PaletteColorIndexes.Coin,
defaultStatusName: 'Backlog',
order: 0
},
task.statusCategory.UnStarted
)
builder.createDoc(
core.class.StatusCategory,
core.space.Model,
{
ofAttribute: task.attribute.State,
label: task.string.StateActive,
icon: task.icon.TaskState,
color: PaletteColorIndexes.Blueberry,
defaultStatusName: 'New state',
@ -431,4 +562,252 @@ export function createModel (builder: Builder): void {
// },
// task.ids.AssigneedNotification
// )
builder.mixin(task.mixin.TaskTypeClass, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.TaskTypeClassPresenter
})
builder.mixin(task.mixin.ProjectTypeClass, core.class.Class, view.mixin.ObjectPresenter, {
presenter: task.component.ProjectTypeClassPresenter
})
builder.mixin(task.class.Task, core.class.Class, setting.mixin.Editable, {
value: true
})
builder.mixin(task.class.Project, core.class.Class, setting.mixin.Editable, {
value: true
})
builder.createDoc(
setting.class.SettingsCategory,
core.space.Model,
{
name: 'statuses',
label: task.string.ManageProjects,
icon: task.icon.ManageTemplates,
component: task.component.ManageProjectsContent,
extraComponents: {
navigation: task.component.ManageProjects,
tools: task.component.ManageProjectsTools
},
group: 'settings-editor',
secured: false,
order: 6000,
expandable: true
},
task.ids.ManageProjects
)
}
/**
* @public
*/
export type FixTaskData = Omit<Data<TaskType>, 'space' | 'statuses' | 'parent'> & { _id?: TaskType['_id'] }
export interface FixTaskResult {
taskTypes: TaskType[]
projectTypes: ProjectType[]
projects: Project[]
}
/**
* @public
*/
export async function fixTaskTypes (
client: MigrationClient,
descriptor: Ref<ProjectTypeDescriptor>,
dataFactory: (t: ProjectType) => Promise<FixTaskData[]>
): Promise<FixTaskResult> {
const categoryObj = client.model.findObject(descriptor)
if (categoryObj === undefined) {
throw new Error('category is not found in model')
}
const projectTypes = await client.find<ProjectType>(DOMAIN_SPACE, {
_class: task.class.ProjectType,
descriptor
})
const baseClassClass = client.hierarchy.getClass(categoryObj.baseClass)
const resultTaskTypes: TaskType[] = []
const resultProjects: Project[] = []
for (const t of projectTypes) {
t.tasks = [...(t.tasks ?? [])]
if (t.targetClass === undefined) {
const targetProjectClassId: Ref<Class<Doc>> = generateId()
t.targetClass = targetProjectClassId
await client.create<TxCreateDoc<Doc>>(DOMAIN_TX, {
_id: generateId(),
objectId: targetProjectClassId,
_class: core.class.TxCreateDoc,
objectClass: core.class.Class,
objectSpace: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
space: core.space.Model,
attributes: {
extends: categoryObj.baseClass,
kind: ClassifierKind.MIXIN,
label: baseClassClass.label,
icon: baseClassClass.icon
}
})
await client.create<TxMixin<Class<ProjectType>, ProjectTypeClass>>(DOMAIN_TX, {
_class: core.class.TxMixin,
_id: generateId(),
space: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
objectId: targetProjectClassId,
objectClass: core.class.Class,
objectSpace: core.space.Model,
mixin: task.mixin.ProjectTypeClass,
attributes: {
projectType: t._id
}
})
await client.update(
DOMAIN_SPACE,
{
_id: t._id
},
{ $set: { targetClass: targetProjectClassId } }
)
}
const dataTypes = await dataFactory(t)
const projects = await client.find<Project>(DOMAIN_SPACE, { type: t._id })
resultProjects.push(...projects)
for (const data of dataTypes) {
const taskTypeId: Ref<TaskType> = data._id ?? generateId()
const descr = client.model.getObject(data.descriptor)
const statuses = await client.find<Status>(DOMAIN_STATUS, {
_id: { $in: t.statuses.map((it) => it._id) },
_class: data.statusClass
})
const dStatuses = [...t.statuses.map((it) => it._id)]
const statusAttr = findStatusAttr(client.hierarchy, data.ofClass)
// Ensure we have at leas't one item in every category.
for (const c of data.statusCategories) {
const cat = await client.model.findOne(core.class.StatusCategory, { _id: c })
const st = statuses.find((it) => it.category === c)
if (st === undefined) {
// We need to add new status into missing category
const statusId: Ref<Status> = generateId()
await client.create<Status>(DOMAIN_STATUS, {
_id: statusId,
_class: data.statusClass,
category: c,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
name: cat?.defaultStatusName ?? 'New state',
space: task.space.Statuses,
ofAttribute: statusAttr._id
})
dStatuses.push(statusId)
await client.update(
DOMAIN_SPACE,
{
_id: t._id
},
{ $push: { statuses: { _id: statusId } } }
)
t.statuses.push({ _id: statusId, taskType: taskTypeId })
}
}
const taskType: TaskType = {
...data,
parent: t._id,
_id: taskTypeId,
_class: task.class.TaskType,
space: t._id,
statuses: dStatuses,
modifiedBy: core.account.System,
modifiedOn: Date.now(),
kind: 'both',
icon: data.icon ?? descr.icon
}
const ofClassClass = client.hierarchy.getClass(data.ofClass)
taskType.icon = ofClassClass.icon
// Create target class for custom field.
const targetClassId: Ref<Class<Doc>> = generateId()
taskType.targetClass = targetClassId
await client.create<TxCreateDoc<Doc>>(DOMAIN_TX, {
_id: generateId(),
objectId: targetClassId,
_class: core.class.TxCreateDoc,
objectClass: core.class.Class,
objectSpace: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
space: core.space.Model,
attributes: {
extends: data.ofClass,
kind: ClassifierKind.MIXIN,
label: getEmbeddedLabel(data.name),
icon: ofClassClass.icon
}
})
await client.create<TxMixin<Class<TaskType>, TaskTypeClass>>(DOMAIN_TX, {
_class: core.class.TxMixin,
_id: generateId(),
space: core.space.Model,
modifiedBy: core.account.ConfigUser,
modifiedOn: Date.now(),
objectId: targetClassId,
objectClass: core.class.Class,
objectSpace: core.space.Model,
mixin: task.mixin.TaskTypeClass,
attributes: {
taskType: taskTypeId,
projectType: t._id
}
})
await client.create(DOMAIN_TASK, taskType)
resultTaskTypes.push(taskType)
await client.update(
DOMAIN_SPACE,
{
_id: t._id
},
{ $push: { tasks: taskTypeId } }
)
t.tasks.push(taskTypeId)
// Update kind and target classId
await client.update(
DOMAIN_TASK,
{ space: { $in: projects.map((it) => it._id) }, _class: data.ofClass },
{ $set: { kind: taskTypeId } }
)
}
// We need to fix project statuses field, for proper icon calculation.
}
for (const t of projectTypes) {
const ttypes = await client.find<TaskType>(DOMAIN_TASK, { _id: { $in: t.tasks } })
const newStatuses = calculateStatuses(t, new Map(ttypes.map((it) => [it._id, it])), [])
await client.update(
DOMAIN_SPACE,
{ _id: t._id },
{
$set: {
statuses: newStatuses
}
}
)
}
return { taskTypes: resultTaskTypes, projectTypes, projects: resultProjects }
}

View File

@ -13,46 +13,20 @@
// limitations under the License.
//
import { TxOperations, type Class, type Doc, type Ref } from '@hcengineering/core'
import {
type Attribute,
type Class,
DOMAIN_STATUS,
DOMAIN_TX,
type Data,
type Doc,
type Ref,
type Space,
type Status,
TxOperations,
generateId,
toIdMap
} from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate,
tryMigrate,
tryUpgrade
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags from '@hcengineering/model-tags'
import {
type Project,
type ProjectStatus,
type ProjectType,
type ProjectTypeCategory,
type Task,
createState,
taskId
} from '@hcengineering/task'
import view, { type Filter } from '@hcengineering/view'
import { DOMAIN_KANBAN, DOMAIN_TASK } from '.'
import { taskId } from '@hcengineering/task'
import task from './plugin'
type ProjectData = Omit<Data<ProjectType>, 'statuses' | 'private' | 'members' | 'archived'>
type OldStatus = Status & { rank: string }
/**
* @public
*/
@ -65,47 +39,6 @@ export async function createSequence (tx: TxOperations, _class: Ref<Class<Doc>>)
}
}
/**
* @public
*/
export async function createProjectType (
client: TxOperations,
data: ProjectData,
states: Data<Status>[],
_id: Ref<ProjectType>,
stateClass: Ref<Class<Status>> = core.class.Status
): Promise<Ref<ProjectType>> {
const current = await client.findOne(task.class.ProjectType, { _id })
if (current !== undefined) {
return current._id
}
const statuses: Ref<Status>[] = []
for (const st of states) {
statuses.push(await createState(client, stateClass, st))
}
const tmpl = await client.createDoc(
task.class.ProjectType,
core.space.Model,
{
description: data.description,
shortDescription: data.shortDescription,
category: data.category,
statuses: statuses.map((p) => {
return { _id: p }
}),
name: data.name,
private: false,
members: [],
archived: false
},
_id
)
return tmpl
}
async function createDefaultSequence (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: task.space.Sequence
@ -151,469 +84,9 @@ async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultStatesSpace(tx)
}
async function renameState (client: MigrationClient): Promise<void> {
const toUpdate = await client.find(DOMAIN_TASK, { state: { $exists: true } })
if (toUpdate.length > 0) {
for (const doc of toUpdate) {
await client.update(
DOMAIN_TX,
{ objectId: doc._id },
{ $rename: { 'attributes.state': 'attributes.status', 'operations.state': 'operations.status' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.objectId': doc._id },
{ $rename: { 'tx.attributes.state': 'tx.attributes.status', 'tx.operations.state': 'tx.operations.status' } }
)
}
await client.update(DOMAIN_TASK, { _id: { $in: toUpdate.map((p) => p._id) } }, { $rename: { state: 'status' } })
}
}
async function renameStatePrefs (client: MigrationUpgradeClient): Promise<void> {
const txop = new TxOperations(client, core.account.System)
const prefs = await client.findAll(view.class.ViewletPreference, {})
for (const pref of prefs) {
let update = false
const config = pref.config
for (let index = 0; index < config.length; index++) {
const conf = config[index]
if (typeof conf === 'string') {
if (conf === 'state') {
config[index] = 'status'
update = true
} else if (conf === '$lookup.state') {
config[index] = '$lookup.status'
update = true
}
} else if (conf.key === 'state') {
conf.key = 'status'
update = true
}
}
if (update) {
await txop.update(pref, {
config
})
}
}
const res = await client.findAll(view.class.FilteredView, { filters: /"key":"state"/ as any })
if (res.length > 0) {
for (const doc of res) {
const filters = JSON.parse(doc.filters) as Filter[]
for (const filter of filters) {
if (filter.key.key === 'state') {
filter.key.key = 'status'
}
}
await txop.update(doc, {
filters: JSON.stringify(filters)
})
}
}
}
async function fixStatusAttributes (client: MigrationClient): Promise<void> {
const spaces = await client.find<Space>(DOMAIN_SPACE, {})
const map = toIdMap(spaces)
const oldStatuses = await client.find<OldStatus>(DOMAIN_STATUS, { ofAttribute: { $exists: false } })
for (const oldStatus of oldStatuses) {
const space = map.get(oldStatus.space)
if (space !== undefined) {
try {
let ofAttribute = task.attribute.State
if (space._class === ('recruit:class:Vacancy' as Ref<Class<Space>>)) {
ofAttribute = 'recruit:attribute:State' as Ref<Attribute<Status>>
}
if (space._class === ('lead:class:Funnel' as Ref<Class<Space>>)) {
ofAttribute = 'lead:attribute:State' as Ref<Attribute<Status>>
}
if (space._class === ('board:class:Board' as Ref<Class<Space>>)) {
ofAttribute = 'board:attribute:State' as Ref<Attribute<Status>>
}
if (space._class === ('tracker:class:Project' as Ref<Class<Space>>)) {
ofAttribute = 'tracker:attribute:IssueStatus' as Ref<Attribute<Status>>
}
if (ofAttribute !== oldStatus.ofAttribute) {
await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute })
}
} catch (err) {
console.log(err)
}
}
}
}
function getTemplateOfAttribute (space: Ref<Space>): Ref<Attribute<Status>> {
let ofAttribute = task.attribute.State
if (space === ('recruit:space:VacancyTemplates' as Ref<Space>)) {
ofAttribute = 'recruit:attribute:State' as Ref<Attribute<Status>>
}
if (space === ('lead:space:FunnelTemplates' as Ref<Space>)) {
ofAttribute = 'lead:attribute:State' as Ref<Attribute<Status>>
}
if (space === ('board:space:BoardTemplates' as Ref<Space>)) {
ofAttribute = 'board:attribute:State' as Ref<Attribute<Status>>
}
return ofAttribute
}
async function fixStatusDoneAttributes (client: MigrationClient): Promise<void> {
const oldStatuses = await client.find<OldStatus>(DOMAIN_STATUS, {})
for (const oldStatus of oldStatuses) {
if (!oldStatus.ofAttribute.includes('DoneState')) continue
const ofAttribute = oldStatus.ofAttribute.replace('DoneState', 'State')
await client.update(DOMAIN_STATUS, { _id: oldStatus._id }, { ofAttribute })
}
}
async function removeDoneStatuses (client: MigrationClient): Promise<void> {
const tasks = await client.find<Task>(DOMAIN_TASK, { doneState: { $exists: true } })
for (const task of tasks) {
if ((task as any).doneState != null) {
await client.update(DOMAIN_TASK, { _id: task._id }, { status: (task as any).doneState, isDone: true })
}
await client.update(DOMAIN_TASK, { _id: task._id }, { $unset: { doneState: '' } })
}
await client.update(
DOMAIN_TX,
{ 'tx.operations.doneState': { $ne: null } },
{ $rename: { 'tx.operations.doneState': 'tx.operations.status' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.attributes.doneState': { $ne: null } },
{ $rename: { 'tx.attributes.doneState': 'tx.attributes.status' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.operations.doneState': { $exists: true } },
{ $unset: { 'tx.operations.doneState': '' } }
)
await client.update(
DOMAIN_TX,
{ 'tx.attributes.doneState': { $exists: true } },
{ $unset: { 'tx.attributes.doneState': '' } }
)
// we need join doneStates to states for all projects
const classes = client.hierarchy.getDescendants(task.class.Project)
const projects = await client.find<Project>(DOMAIN_SPACE, { _class: { $in: classes }, doneStates: { $exists: true } })
for (const project of projects) {
await client.update(
DOMAIN_SPACE,
{ _id: project._id },
{ states: (project as any).states.concat((project as any).doneStates) }
)
await client.update(DOMAIN_SPACE, { _id: project._id }, { $unset: { doneStates: '' } })
}
}
async function removeStateClass (client: MigrationClient): Promise<void> {
await client.update<Status>(
DOMAIN_STATUS,
{ _class: 'task:class:State' as Ref<Class<Doc>> },
{ _class: core.class.Status, category: task.statusCategory.Active }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:State' }, { objectClass: core.class.Status })
await client.update(
DOMAIN_STATUS,
{ _class: 'task:class:WonState' as Ref<Class<Doc>> },
{ _class: core.class.Status, category: task.statusCategory.Won }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:WonState' }, { objectClass: core.class.Status })
await client.update(
DOMAIN_STATUS,
{ _class: 'task:class:LostState' as Ref<Class<Doc>> },
{ _class: core.class.Status, category: task.statusCategory.Lost }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:LostState' }, { objectClass: core.class.Status })
await client.update(
DOMAIN_STATUS,
{ _class: 'task:class:DoneState' as Ref<Class<Doc>> },
{ _class: core.class.Status }
)
await client.update(DOMAIN_TX, { objectClass: 'task:class:DoneState' }, { objectClass: core.class.Status })
}
async function migrateTemplatesToTypes (client: MigrationClient): Promise<void> {
interface KanbanTemplate extends Doc {
title: string
description?: string
shortDescription?: string
}
interface KanbanTemplateSpace extends Space {
attachedToClass: Ref<Class<Doc>>
}
interface StateTemplate extends Doc, Status {
attachedTo: Ref<KanbanTemplate>
rank: string
}
const classes = client.hierarchy.getDescendants(task.class.Project)
const templates = await client.find<KanbanTemplate>(DOMAIN_KANBAN, {
_class: 'task:class:KanbanTemplate' as Ref<Class<Doc>>
})
for (const template of templates) {
const used = await client.find(DOMAIN_SPACE, { templateId: template._id })
if (used.length === 0) {
await client.delete(DOMAIN_KANBAN, template._id)
continue
}
const space = (
await client.find<KanbanTemplateSpace>(DOMAIN_SPACE, { _id: template.space as Ref<KanbanTemplateSpace> })
)[0]
if (space === undefined) continue
const states = await client.find<StateTemplate>(
DOMAIN_KANBAN,
{ _class: 'task:class:StateTemplate' as Ref<Class<Doc>>, attachedTo: template._id },
{ sort: { rank: 1 } }
)
const wonStates = await client.find<StateTemplate>(
DOMAIN_KANBAN,
{ _class: 'task:class:WonStateTemplate' as Ref<Class<Doc>>, attachedTo: template._id },
{ sort: { rank: 1 } }
)
const lostStates = await client.find<StateTemplate>(
DOMAIN_KANBAN,
{ _class: 'task:class:LostStateTemplate' as Ref<Class<Doc>>, attachedTo: template._id },
{ sort: { rank: 1 } }
)
const statuses: ProjectStatus[] = []
const currentStates = await client.find<Status>(DOMAIN_STATUS, {})
for (const st of states) {
const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase())
if (exists !== undefined) {
statuses.push({ _id: exists._id, color: st.color })
} else {
const id = generateId<Status>()
await client.create<Status>(DOMAIN_STATUS, {
ofAttribute: getTemplateOfAttribute(st.space),
name: st.name,
_id: id,
space: task.space.Statuses,
modifiedOn: st.modifiedOn,
modifiedBy: st.modifiedBy,
_class: core.class.Status,
color: st.color ?? 9,
createdBy: st.createdBy,
createdOn: st.createdOn,
category: task.statusCategory.Active
})
statuses.push({ _id: id, color: st.color })
}
}
for (const st of wonStates) {
const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase())
if (exists !== undefined) {
statuses.push({ _id: exists._id, color: st.color })
} else {
const id = generateId<Status>()
await client.create<Status>(DOMAIN_STATUS, {
ofAttribute: getTemplateOfAttribute(st.space),
name: st.name,
_id: id,
space: task.space.Statuses,
modifiedOn: st.modifiedOn,
modifiedBy: st.modifiedBy,
_class: core.class.Status,
color: st.color ?? 15,
createdBy: st.createdBy,
createdOn: st.createdOn,
category: task.statusCategory.Won
})
statuses.push({ _id: id, color: st.color })
}
}
for (const st of lostStates) {
const exists = currentStates.find((p) => p.name.toLocaleLowerCase() === st.name.toLocaleLowerCase())
if (exists !== undefined) {
statuses.push({ _id: exists._id, color: st.color })
} else {
const id = generateId<Status>()
await client.create<Status>(DOMAIN_STATUS, {
ofAttribute: getTemplateOfAttribute(st.space),
name: st.name,
_id: id,
space: task.space.Statuses,
modifiedOn: st.modifiedOn,
modifiedBy: st.modifiedBy,
_class: core.class.Status,
color: st.color ?? 0,
createdBy: st.createdBy,
createdOn: st.createdOn,
category: task.statusCategory.Lost
})
statuses.push({ _id: id, color: st.color })
}
}
const category = await getProjectTypeCategory(client, space.attachedToClass)
await client.create<ProjectType>(DOMAIN_SPACE, {
name: template.title,
description: template.description ?? '',
shortDescription: template.shortDescription,
category: category ?? (template.space as Ref<Doc> as Ref<ProjectTypeCategory>),
private: false,
members: [],
archived: false,
_id: template._id as Ref<Doc> as Ref<ProjectType>,
space: core.space.Space,
modifiedOn: template.modifiedOn,
modifiedBy: template.modifiedBy,
_class: task.class.ProjectType,
statuses
})
await client.delete(DOMAIN_KANBAN, template._id)
}
// we should found all projects without types and has templateID we should just rename it to type (if it exists)
const projectsWithTemplate = await client.find<Project>(DOMAIN_SPACE, {
_class: { $in: classes },
type: { $exists: false },
templateId: { $exists: true }
})
for (const project of projectsWithTemplate) {
await client.update(DOMAIN_SPACE, { _id: project._id }, { type: (project as any).templateId })
}
// we should remove all state templates
const stateClasses = [
'task:class:StateTemplate' as Ref<Class<Doc>>,
'task:class:WonStateTemplate' as Ref<Class<Doc>>,
'task:class:LostStateTemplate' as Ref<Class<Doc>>
]
const states = await client.find(DOMAIN_KANBAN, { _class: { $in: stateClasses } })
for (const st of states) {
await client.delete(DOMAIN_KANBAN, st._id)
}
}
async function getProjectTypeCategory (
client: MigrationClient,
_class: Ref<Class<Project>>
): Promise<Ref<ProjectTypeCategory> | undefined> {
let clazz = _class
while (true) {
const res = await client.model.findAll(task.class.ProjectTypeCategory, { attachedToClass: clazz })
if (res[0] !== undefined) return res[0]._id
const parent = client.hierarchy.getClass(clazz)
if (parent.extends === undefined) return
clazz = parent.extends
}
}
async function migrateProjectTypes (client: MigrationClient): Promise<void> {
const classes = client.hierarchy.getDescendants(task.class.Project)
// we should found all projects without types and group by class and then by statuses and create new type
const projects = await client.find<Project>(DOMAIN_SPACE, { _class: { $in: classes }, type: { $exists: false } })
const projectsByCategory = new Map<Ref<ProjectTypeCategory>, Project[]>()
for (const project of projects) {
const category = await getProjectTypeCategory(client, project._class)
if (category === undefined) continue
const arr = projectsByCategory.get(category) ?? []
arr.push(project)
projectsByCategory.set(category, arr)
}
for (const [category, projects] of projectsByCategory) {
const gouped = group(projects)
for (const gr of gouped) {
await client.create<ProjectType>(DOMAIN_SPACE, {
category,
name: gr.projects[0].name,
description: '',
private: false,
members: [],
archived: false,
_id: gr.type,
space: core.space.Space,
modifiedOn: Date.now(),
modifiedBy: core.account.System,
_class: task.class.ProjectType,
statuses: gr.statuses.map((p) => {
return { _id: p }
})
})
await client.update(DOMAIN_SPACE, { _id: { $in: gr.projects.map((p) => p._id) } }, { type: gr.type })
}
}
// we need remove states from all projects
const allProjects = await client.find<Project>(DOMAIN_SPACE, { _class: { $in: classes } })
await Promise.all(
allProjects.map(async (project) => {
await client.update(
DOMAIN_SPACE,
{ _id: project._id },
{ $unset: { states: '', templateId: '', doneStates: '' } }
)
})
)
}
interface ProjectTypeGroup {
statuses: Ref<Status>[]
projects: Project[]
type: Ref<ProjectType>
}
function group (projects: Project[]): ProjectTypeGroup[] {
const map = new Map<string, ProjectTypeGroup>()
for (const project of projects) {
const ids = getIds((project as any).states ?? [])
const obj: ProjectTypeGroup = map.get(ids) ?? {
statuses: (project as any).states ?? [],
projects: [],
type: generateId<ProjectType>()
}
obj.projects.push(project)
map.set(ids, obj)
}
return Array.from(map.values())
}
function getIds (states: Ref<Status>[]): string {
return states.join(',')
}
export const taskOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, taskId, [
{
state: 'fixStatusAttributes',
func: fixStatusAttributes
},
{
state: 'renameState',
func: renameState
},
{
state: 'removeDoneStatuses',
func: removeDoneStatuses
},
{
state: 'fixStatusDoneAttributes',
func: fixStatusDoneAttributes
},
{
state: 'removeStateClass',
func: removeStateClass
},
{
state: 'migrateTemplatesToTypes',
func: migrateTemplatesToTypes
},
{
state: 'migrateProjectTypes',
func: migrateProjectTypes
},
{
state: 'projectTypeSpace',
func: async (client) => {
@ -640,11 +113,6 @@ export const taskOperation: MigrateOperation = {
task.category.TaskTag
)
await tryUpgrade(client, taskId, [
{
state: 'renameStatePrefs',
func: renameStatePrefs
}
])
await tryUpgrade(client, taskId, [])
}
}

View File

@ -14,8 +14,9 @@
// limitations under the License.
//
import {} from '@hcengineering/notification'
import type { Ref, Space } from '@hcengineering/core'
import { mergeIds } from '@hcengineering/platform'
import { mergeIds, type IntlString } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import { taskId } from '@hcengineering/task'
import task from '@hcengineering/task-resources/src/plugin'
@ -59,7 +60,14 @@ export default mergeIds(taskId, task, {
StatusSelector: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
TypesView: '' as AnyComponent,
StateIconPresenter: '' as AnyComponent
StateIconPresenter: '' as AnyComponent,
TaskTypePresenter: '' as AnyComponent,
ProjectTypePresenter: '' as AnyComponent,
TaskTypeClassPresenter: '' as AnyComponent,
ProjectTypeClassPresenter: '' as AnyComponent,
ManageProjects: '' as AnyComponent,
ManageProjectsTools: '' as AnyComponent,
ManageProjectsContent: '' as AnyComponent
},
space: {
TasksPublic: '' as Ref<Space>
@ -67,5 +75,10 @@ export default mergeIds(taskId, task, {
viewlet: {
TableIssue: '' as Ref<Viewlet>,
KanbanIssue: '' as Ref<Viewlet>
},
string: {
ManageProjects: '' as IntlString,
StateBacklog: '' as IntlString,
StateActive: '' as IntlString
}
})

View File

@ -211,6 +211,7 @@ function defineFilters (builder: Builder): void {
//
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
filters: [
'kind',
'status',
'priority',
'space',
@ -359,7 +360,8 @@ function defineApplication (
['all', tracker.string.All, {}],
['active', tracker.string.Active, {}],
['backlog', tracker.string.Backlog, {}]
]
],
allProjectsTypes: true
}
},
{
@ -368,7 +370,6 @@ function defineApplication (
icon: view.icon.Archive,
label: tracker.string.AllProjects,
position: 'bottom',
visibleIf: workbench.function.IsOwner,
spaceClass: tracker.class.Project,
componentProps: {
_class: tracker.class.Project,
@ -686,22 +687,27 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
task.class.ProjectTypeCategory,
task.class.ProjectTypeDescriptor,
core.space.Model,
{
name: tracker.string.Projects,
name: tracker.string.TrackerApplication,
description: tracker.string.ManageWorkflowStatuses,
icon: task.component.TemplatesIcon,
attachedToClass: tracker.class.Project,
statusClass: tracker.class.IssueStatus,
statusCategories: [
tracker.issueStatusCategory.Backlog,
tracker.issueStatusCategory.Unstarted,
tracker.issueStatusCategory.Started,
tracker.issueStatusCategory.Completed,
tracker.issueStatusCategory.Canceled
]
baseClass: tracker.class.Project
},
tracker.category.ProjectTypeCategory
tracker.descriptors.ProjectType
)
builder.createDoc(
task.class.TaskTypeDescriptor,
core.space.Model,
{
baseClass: tracker.class.Issue,
allowCreate: true,
description: tracker.string.Issue,
icon: tracker.icon.Issue,
name: tracker.string.Issue
},
tracker.descriptors.Issue
)
}

View File

@ -13,31 +13,20 @@
// limitations under the License.
//
import core, {
DOMAIN_TX,
type Data,
type Ref,
SortingOrder,
type Status,
type TxCollectionCUD,
type TxCreateDoc,
TxOperations,
type TxUpdateDoc,
toIdMap
} from '@hcengineering/core'
import core, { SortingOrder, TxOperations, generateId, type Data, type Ref, type Status } from '@hcengineering/core'
import {
createOrUpdate,
tryMigrate,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate,
tryMigrate
type MigrationUpgradeClient
} from '@hcengineering/model'
import { DOMAIN_TASK, createProjectType } from '@hcengineering/model-task'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import { createProjectType, fixTaskTypes } from '@hcengineering/model-task'
import tags from '@hcengineering/tags'
import { type Issue, TimeReportDayType, type TimeSpendReport } from '@hcengineering/tracker'
import view from '@hcengineering/view'
import task, { type TaskType } from '@hcengineering/task'
import { TimeReportDayType } from '@hcengineering/tracker'
import tracker from './plugin'
import { DOMAIN_TRACKER } from './types'
async function createDefaultProject (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Project, {
@ -48,14 +37,12 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
objectId: tracker.project.DefaultProject
})
// Create new if not deleted by customers.
if (current === undefined && currentDeleted === undefined) {
if ((await tx.findOne(task.class.ProjectType, { _id: tracker.ids.ClassingProjectType })) === undefined) {
const categories = await tx.findAll(
core.class.StatusCategory,
{ ofAttribute: tracker.attribute.IssueStatus },
{ sort: { order: SortingOrder.Ascending } }
)
const states: Omit<Data<Status>, 'rank'>[] = []
for (const category of categories) {
@ -65,22 +52,85 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
category: category._id
})
}
await createProjectType(
tx,
{
name: 'Classic project',
descriptor: tracker.descriptors.ProjectType,
description: '',
tasks: []
},
[
{
_id: tracker.taskTypes.Issue,
descriptor: tracker.descriptors.Issue,
name: 'Issue',
factory: states,
ofClass: tracker.class.Issue,
targetClass: tracker.class.Issue,
statusCategories: categories.map((it) => it._id),
statusClass: core.class.Status,
kind: 'both',
allowedAsChildOf: [tracker.taskTypes.Issue]
}
],
tracker.ids.ClassingProjectType
)
}
const typeId = await createProjectType(
if ((await tx.findOne(task.class.ProjectType, { _id: tracker.ids.BaseProjectType })) === undefined) {
const issueId: Ref<TaskType> = generateId()
const baseCategories = [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
]
const categories = await tx.findAll(
core.class.StatusCategory,
{ _id: { $in: baseCategories } },
{ sort: { order: SortingOrder.Ascending } }
)
const states: Omit<Data<Status>, 'rank'>[] = []
for (const category of categories) {
states.push({
ofAttribute: tracker.attribute.IssueStatus,
name: category.defaultStatusName,
category: category._id
})
}
await createProjectType(
tx,
{
name: 'Base project',
category: tracker.category.ProjectTypeCategory,
description: ''
descriptor: tracker.descriptors.ProjectType,
description: '',
tasks: []
},
states,
tracker.ids.BaseProjectType,
tracker.class.IssueStatus
[
{
_id: issueId,
name: 'Issue',
descriptor: tracker.descriptors.Issue,
factory: states,
ofClass: tracker.class.Issue,
targetClass: tracker.class.Issue,
statusCategories: baseCategories,
statusClass: core.class.Status,
kind: 'both',
allowedAsChildOf: [issueId]
}
],
tracker.ids.BaseProjectType
)
}
// Create new if not deleted by customers.
if (current === undefined && currentDeleted === undefined) {
const state = await tx.findOne(
tracker.class.IssueStatus,
{ space: typeId },
{ space: tracker.ids.DefaultProjectType },
{ sort: { rank: SortingOrder.Ascending } }
)
if (state !== undefined) {
@ -98,7 +148,7 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
defaultIssueStatus: state._id,
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
defaultAssignee: undefined,
type: typeId
type: tracker.ids.DefaultProjectType
},
tracker.project.DefaultProject
)
@ -123,184 +173,55 @@ async function createDefaults (tx: TxOperations): Promise<void> {
)
}
async function fixIconsWithEmojis (tx: TxOperations): Promise<void> {
const projectsWithWrongIcon = await tx.findAll(tracker.class.Project, { icon: tracker.component.IconWithEmoji })
const promises = []
for (const project of projectsWithWrongIcon) {
promises.push(tx.update(project, { icon: view.ids.IconWithEmoji }))
}
await Promise.all(promises)
}
async function fixSpentTime (client: MigrationClient): Promise<void> {
const issues = await client.find<Issue>(DOMAIN_TASK, { reportedTime: { $gt: 0 } })
for (const issue of issues) {
const childInfo = issue.childInfo
for (const child of childInfo ?? []) {
child.reportedTime = child.reportedTime * 8
}
await client.update(DOMAIN_TASK, { _id: issue._id }, { reportedTime: issue.reportedTime * 8, childInfo })
}
const reports = await client.find<TimeSpendReport>(DOMAIN_TRACKER, {})
for (const report of reports) {
await client.update(DOMAIN_TRACKER, { _id: report._id }, { value: report.value * 8 })
}
const createTxes = await client.find<TxCollectionCUD<Issue, TimeSpendReport>>(DOMAIN_TX, {
'tx.objectClass': tracker.class.TimeSpendReport,
'tx._class': core.class.TxCreateDoc,
'tx.attributes.value': { $exists: true }
})
for (const tx of createTxes) {
await client.update(
DOMAIN_TX,
{ _id: tx._id },
{ 'tx.attributes.value': (tx.tx as TxCreateDoc<TimeSpendReport>).attributes.value * 8 }
)
}
const updateTxes = await client.find<TxCollectionCUD<Issue, TimeSpendReport>>(DOMAIN_TX, {
'tx.objectClass': tracker.class.TimeSpendReport,
'tx._class': core.class.TxUpdateDoc,
'tx.operations.value': { $exists: true }
})
for (const tx of updateTxes) {
const val = (tx.tx as TxUpdateDoc<TimeSpendReport>).operations.value
if (val !== undefined) {
await client.update(DOMAIN_TX, { _id: tx._id }, { 'tx.operations.value': val * 8 })
}
}
}
async function fixEstimation (client: MigrationClient): Promise<void> {
const issues = await client.find<Issue>(DOMAIN_TASK, { estimation: { $gt: 0 } })
for (const issue of issues) {
const childInfo = issue.childInfo
for (const child of childInfo ?? []) {
child.estimation = child.estimation * 8
}
await client.update(DOMAIN_TASK, { _id: issue._id }, { estimation: issue.estimation * 8, childInfo })
}
const createTxes = await client.find<TxCollectionCUD<Issue, Issue>>(DOMAIN_TX, {
'tx.objectClass': tracker.class.Issue,
'tx._class': core.class.TxCreateDoc,
'tx.attributes.estimation': { $gt: 0 }
})
for (const tx of createTxes) {
await client.update(
DOMAIN_TX,
{ _id: tx._id },
{ 'tx.attributes.estimation': (tx.tx as TxCreateDoc<Issue>).attributes.estimation * 8 }
)
}
const updateTxes = await client.find<TxCollectionCUD<Issue, Issue>>(DOMAIN_TX, {
'tx.objectClass': tracker.class.Issue,
'tx._class': core.class.TxUpdateDoc,
'tx.operations.estimation': { $exists: true }
})
for (const tx of updateTxes) {
const val = (tx.tx as TxUpdateDoc<Issue>).operations.estimation
if (val !== undefined) {
await client.update(DOMAIN_TX, { _id: tx._id }, { 'tx.operations.estimation': val * 8 })
}
}
}
async function fixRemainingTime (client: MigrationClient): Promise<void> {
while (true) {
const issues = await client.find<Issue>(
DOMAIN_TASK,
{ _class: tracker.class.Issue, remainingTime: { $exists: false } },
{ limit: 1000 }
)
for (const issue of issues) {
await client.update(
DOMAIN_TASK,
{ _id: issue._id },
{ remainingTime: Math.max(0, issue.estimation - issue.reportedTime) }
)
}
if (issues.length === 0) {
break
}
}
await client.update(
DOMAIN_TASK,
{ _class: { $ne: tracker.class.Issue }, remainingTime: { $exists: true } },
{ $unset: { remainingTime: '' } }
)
}
async function fixParentsSpace (client: MigrationClient): Promise<void> {
while (true) {
const issues = await client.find<Issue>(
DOMAIN_TASK,
{ _class: tracker.class.Issue, 'parents.space': { $exists: false }, parents: { $exists: true, $ne: [] } },
{ limit: 1000 }
)
const parentIds = new Set<Ref<Issue>>()
for (const i of issues) {
for (const p of i.parents ?? []) {
parentIds.add(p.parentId)
async function fixTrackerTaskTypes (client: MigrationClient): Promise<void> {
await fixTaskTypes(client, tracker.descriptors.ProjectType, async (t) => {
const typeId: Ref<TaskType> = generateId()
return [
{
_id: typeId,
name: 'Issue',
descriptor: tracker.descriptors.Issue,
ofClass: tracker.class.Issue,
targetClass: tracker.class.Issue,
statusCategories: [
tracker.issueStatusCategory.Backlog,
tracker.issueStatusCategory.Unstarted,
tracker.issueStatusCategory.Started,
tracker.issueStatusCategory.Completed,
tracker.issueStatusCategory.Canceled
],
statusClass: tracker.class.IssueStatus,
kind: 'task',
allowedAsChildOf: [typeId]
}
}
const parentIssues = toIdMap(
await client.find<Issue>(DOMAIN_TASK, { _class: tracker.class.Issue, _id: { $in: Array.from(parentIds) } })
)
for (const issue of issues) {
await client.update(
DOMAIN_TASK,
{ _id: issue._id },
{ parents: issue.parents.map((it) => ({ ...it, space: parentIssues.get(it.parentId)?.space ?? it.space })) }
)
}
if (issues.length === 0) {
break
}
}
await client.update(
DOMAIN_TASK,
{ _class: { $ne: tracker.class.Issue }, remainingTime: { $exists: true } },
{ $unset: { remainingTime: '' } }
)
}
async function moveIssues (client: MigrationClient): Promise<void> {
const docs = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue })
if (docs.length > 0) {
await client.move(DOMAIN_TRACKER, { _class: tracker.class.Issue }, DOMAIN_TASK)
}
]
})
}
export const trackerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, 'tracker', [
{
state: 'moveIssues',
func: moveIssues
state: 'fix-category-descriptors',
func: async (client) => {
await client.update(
DOMAIN_SPACE,
{ _class: task.class.ProjectType, category: 'tracker:category:ProjectTypeCategory' },
{
$set: { descriptor: tracker.descriptors.ProjectType },
$unset: { category: 1 }
}
)
}
},
{
state: 'reportTimeDayToHour',
func: fixSpentTime
},
{
state: 'estimationDayToHour',
func: fixEstimation
},
{
state: 'fixRemainingTime',
func: fixRemainingTime
},
{
state: 'fixParentsSpace',
func: fixParentsSpace
state: 'fixTaskTypes',
func: fixTrackerTaskTypes
}
])
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)
await fixIconsWithEmojis(tx)
}
}

View File

@ -13,18 +13,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type DocUpdateMessageViewlet, type TxViewlet } from '@hcengineering/activity'
import { type ChatMessageViewlet } from '@hcengineering/chunter'
import { type Doc, type Ref } from '@hcengineering/core'
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform'
import { type ProjectType } from '@hcengineering/task'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
import { type ProjectType, type TaskTypeDescriptor } from '@hcengineering/task'
import { trackerId } from '@hcengineering/tracker'
import tracker from '@hcengineering/tracker-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view'
import { type Application } from '@hcengineering/workbench'
import { type DocUpdateMessageViewlet, type TxViewlet } from '@hcengineering/activity'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { type ChatMessageViewlet } from '@hcengineering/chunter'
export default mergeIds(trackerId, tracker, {
string: {
@ -59,7 +59,8 @@ export default mergeIds(trackerId, tracker, {
MilestoneFilter: '' as AnyComponent,
EditRelatedTargets: '' as AnyComponent,
EditRelatedTargetsPopup: '' as AnyComponent,
IssueSearchIcon: '' as AnyComponent
IssueSearchIcon: '' as AnyComponent,
MembersArrayEditor: '' as AnyComponent
},
app: {
Tracker: '' as Ref<Application>
@ -85,7 +86,9 @@ export default mergeIds(trackerId, tracker, {
IssueChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
IssueTemplateChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
ComponentChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
MilestoneChatMessageViewlet: '' as Ref<ChatMessageViewlet>
MilestoneChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
ClassingProjectType: '' as Ref<ProjectType>,
DefaultProjectType: '' as Ref<ProjectType>
},
completion: {
IssueQuery: '' as Resource<ObjectSearchFactory>,
@ -106,5 +109,8 @@ export default mergeIds(trackerId, tracker, {
DeleteProject: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteProjectClean: '' as Ref<Action<Doc, Record<string, any>>>,
DeleteIssue: '' as Ref<Action<Doc, Record<string, any>>>
},
descriptors: {
Issue: '' as Ref<TaskTypeDescriptor>
}
})

View File

@ -69,7 +69,7 @@ import chunter from '@hcengineering/chunter'
export const DOMAIN_TRACKER = 'tracker' as Domain
@Model(tracker.class.IssueStatus, core.class.Status)
@UX(tracker.string.IssueStatuses, undefined, undefined, 'rank', 'name')
@UX(tracker.string.IssueStatus, undefined, undefined, 'rank', 'name')
export class TIssueStatus extends TStatus implements IssueStatus {}
/**
* @public
@ -120,7 +120,7 @@ export class TProject extends TTaskProject implements Project {
declare defaultTimeReportDay: TimeReportDayType
@Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssue)
@Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssues)
relatedIssueTargets!: number
}
/**

View File

@ -26,6 +26,7 @@ import tags from '@hcengineering/tags'
export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
groupBy: [
'status',
'kind',
'assignee',
'priority',
'component',
@ -38,6 +39,7 @@ export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
],
orderBy: [
['status', SortingOrder.Ascending],
['kind', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending],
@ -95,6 +97,13 @@ export function issueConfig (
props: { kind: 'list', size: 'small', justify: 'center' },
displayProps: { key: key + 'status' }
},
// {
// key: 'kind',
// label: task.string.TaskType,
// presenter: task.component.TaskTypePresenter,
// props: { kind: 'list', size: 'small', justify: 'center' },
// displayProps: { key: key + 'kind' }
// },
{
key: '',
label: tracker.string.Title,
@ -230,9 +239,10 @@ export function defineViewlets (builder: Builder): void {
)
const subIssuesOptions: ViewOptionsModel = {
groupBy: ['status', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
groupBy: ['status', 'kind', 'assignee', 'priority', 'milestone', 'createdBy', 'modifiedBy'],
orderBy: [
['rank', SortingOrder.Ascending],
['kind', SortingOrder.Ascending],
['status', SortingOrder.Ascending],
['priority', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
@ -508,8 +518,9 @@ export function defineViewlets (builder: Builder): void {
attachTo: tracker.class.Project,
descriptor: view.viewlet.List,
viewOptions: {
groupBy: ['createdBy'],
groupBy: ['type', 'createdBy'],
orderBy: [
['type', SortingOrder.Descending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
@ -524,7 +535,26 @@ export function defineViewlets (builder: Builder): void {
key: '',
props: { kind: 'list' }
},
{ key: '', displayProps: { grow: true } }
{ key: '', displayProps: { grow: true } },
{
key: '',
presenter: tracker.component.MembersArrayEditor,
sortingKey: 'members',
props: { readonly: true, kind: 'list' }
},
{
key: 'type',
props: { kind: 'list' }
},
{
key: 'defaultAssignee',
props: { kind: 'list' }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
displayProps: { fixed: 'right', dividerBefore: true }
}
]
},
tracker.viewlet.ProjectList

View File

@ -427,7 +427,11 @@ export class Hierarchy {
this.classifierProperties.delete(attribute.attributeOf)
}
getAllAttributes (clazz: Ref<Classifier>, to?: Ref<Classifier>): Map<string, AnyAttribute> {
getAllAttributes (
clazz: Ref<Classifier>,
to?: Ref<Classifier>,
traverse?: (name: string, attr: AnyAttribute) => void
): Map<string, AnyAttribute> {
const result = new Map<string, AnyAttribute>()
let ancestors = this.getAncestors(clazz)
if (to !== undefined) {
@ -448,6 +452,7 @@ export class Hierarchy {
const attributes = this.attributes.get(cls)
if (attributes !== undefined) {
for (const [name, attr] of attributes) {
traverse?.(name, attr)
result.set(name, attr)
}
}

View File

@ -7,5 +7,5 @@
"@typescript-eslint/array-type": "off",
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/consistent-type-imports": "off"
}
}
}

View File

@ -19,6 +19,14 @@
},
"plugins": ["@typescript-eslint", "import"],
"ignorePatterns": ["*.json", "node_modules/*", ".eslintrc.js"],
"settings": {
"import/resolver": {
"node": {
"extensions": [".ts"],
"moduleDirectory": ["src", "node_modules"]
}
}
},
"overrides": [
{
"files": ["**/*.svelte"],
@ -30,13 +38,13 @@
"@typescript-eslint/array-type": "off",
"@typescript-eslint/promise-function-async": "off",
"@typescript-eslint/consistent-type-imports": "off",
"import/first": "off",
"import/no-duplicates": "off",
"import/first": "warn",
"import/no-duplicates": "warn",
"import/no-mutable-exports": "off",
"import/no-unresolved": "off",
"no-multiple-empty-lines": "off",
"import/no-unresolved": "warn",
"no-multiple-empty-lines": "warn",
"no-undef-init": "off",
"no-use-before-define": "off",
"no-use-before-define": "warn",
// This need to be enabled eventually
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/strict-boolean-expressions":"warn",

View File

@ -21,7 +21,7 @@ import type { IntlString, Plugin } from './platform'
import { Severity, Status, unknownError } from './status'
import { getMetadata } from './metadata'
import platform from './platform'
import platform, { _EmbeddedId } from './platform'
/**
* @public
@ -119,7 +119,7 @@ export async function translate<P extends Record<string, any>> (
} else {
try {
const id = _parseId(message)
if (id.component === 'embedded') {
if (id.component === _EmbeddedId) {
return id.name
}
const translation = (await getTranslation(id, locale)) ?? message

View File

@ -17,9 +17,9 @@
// import core from '@hcengineering/core'
import type { Class, Doc, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import type { AnySvelteComponent } from '@hcengineering/ui'
import type { AnySvelteComponent, EditStyle } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes'
import { KeyedAttribute, getAttribute, updateAttribute } from '../attributes'
import { getAttributePresenterClass, getClient } from '../utils'
export let _class: Ref<Class<Doc>>
@ -29,6 +29,7 @@
export let focus: boolean = false
export let editable = true
export let focusIndex = -1
export let editKind: EditStyle | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -42,7 +43,9 @@
$: if (presenterClass !== undefined) {
const typeClass = hierarchy.getClass(presenterClass.attrClass)
const editorMixin = hierarchy.as(typeClass, view.mixin.AttributeEditor)
editor = getResource(editorMixin.inlineEditor)
if (editorMixin.inlineEditor !== undefined) {
editor = getResource(editorMixin.inlineEditor)
}
}
function onChange (value: any) {
@ -67,6 +70,7 @@
{onChange}
{focus}
{focusIndex}
{editKind}
/>
{/await}
{/if}

View File

@ -15,10 +15,17 @@
-->
<script lang="ts">
import type { IntlString } from '@hcengineering/platform'
import { Button, IconClose, Label, Scroller } from '@hcengineering/ui'
import {
Button,
IconClose,
Label,
Scroller,
deviceOptionsStore as deviceInfo,
resizeObserver,
IconBack
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import presentation from '..'
import { deviceOptionsStore as deviceInfo, resizeObserver, IconBack } from '@hcengineering/ui'
import IconForward from './icons/Forward.svelte'
export let label: IntlString

View File

@ -1,8 +1,7 @@
<script lang="ts">
import { AnyAttribute, DocIndexState, extractDocKey, isFullTextAttribute } from '@hcengineering/core'
import { Label } from '@hcengineering/ui'
import { Icon } from '@hcengineering/ui'
import { Label, Icon } from '@hcengineering/ui'
import { getClient } from '../utils'
export let indexDoc: DocIndexState

View File

@ -17,10 +17,9 @@
import type { IntlString } from '@hcengineering/platform'
import { translate } from '@hcengineering/platform'
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@hcengineering/ui'
import { showPopup, Button } from '@hcengineering/ui'
import { showPopup, Button, themeStore } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import presentation, { SpacesMultiPopup } from '..'
import { themeStore } from '@hcengineering/ui'
export let selectedItems: Ref<Space>[] = []
export let _classes: Ref<Class<Space>>[] = []

View File

@ -13,30 +13,33 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, ComponentType } from 'svelte'
import { ComponentType, createEventDispatcher } from 'svelte'
import { Class, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { getPlatformColorDef, getPlatformColorForTextDef, IconWithEmoji, themeStore } from '@hcengineering/ui'
import {
AnySvelteComponent,
Button,
ButtonKind,
ButtonSize,
ButtonShape,
ButtonSize,
IconFolder,
IconWithEmoji,
Label,
TooltipAlignment,
eventToHTMLElement,
getEventPositionElement,
getFocusManager,
IconFolder,
Label,
getPlatformColorDef,
getPlatformColorForTextDef,
showPopup,
TooltipAlignment
themeStore
} from '@hcengineering/ui'
import view, { IconProps } from '@hcengineering/view'
import SpacesPopup from './SpacesPopup.svelte'
import { ObjectCreate } from '../types'
import { getClient } from '../utils'
import SpacesPopup from './SpacesPopup.svelte'
export let _class: Ref<Class<Space>>
export let spaceQuery: DocumentQuery<Space> | undefined = { archived: false }
@ -69,20 +72,19 @@
const dispatch = createEventDispatcher()
const mgr = getFocusManager()
async function updateSelected (value: Ref<Space> | undefined) {
selected = value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: value }) : undefined
async function updateSelected (_value: Ref<Space> | undefined, spaceQuery: DocumentQuery<Space> | undefined) {
selected = _value !== undefined ? await client.findOne(_class, { ...(spaceQuery ?? {}), _id: _value }) : undefined
if (selected === undefined && autoSelect) {
selected = (await findDefaultSpace?.()) ?? (await client.findOne(_class, { ...(spaceQuery ?? {}) }))
if (selected !== undefined) {
value = selected._id ?? undefined
dispatch('change', value)
dispatch('space', selected)
}
}
dispatch('object', selected)
}
$: updateSelected(value)
$: updateSelected(value, spaceQuery)
const showSpacesPopup = (ev: MouseEvent) => {
if (readonly) {
@ -108,7 +110,6 @@
(result) => {
if (result !== undefined) {
value = result?._id ?? undefined
dispatch('change', value)
mgr?.setFocusPos(focusIndex)
}
}

View File

@ -15,11 +15,10 @@
<script lang="ts">
import { Class, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { AnySvelteComponent, ButtonKind, ButtonSize, ButtonShape } from '@hcengineering/ui'
import { AnySvelteComponent, ButtonKind, ButtonShape, ButtonSize } from '@hcengineering/ui'
import { ComponentType, createEventDispatcher } from 'svelte'
import { ObjectCreate } from '../types'
import SpaceSelect from './SpaceSelect.svelte'
import { createEventDispatcher } from 'svelte'
import { ComponentType } from 'svelte'
export let space: Ref<Space> | undefined = undefined
export let _class: Ref<Class<Space>>
@ -41,9 +40,13 @@
export let readonly: boolean = false
export let findDefaultSpace: (() => Promise<Space | undefined>) | undefined = undefined
const dispatch = createEventDispatcher()
export let create: ObjectCreate | undefined = undefined
const dispatch = createEventDispatcher()
let _space = space
$: if (_space !== space) {
_space = space
dispatch('change', space)
}
</script>
<SpaceSelect
@ -67,10 +70,6 @@
{iconWithEmoji}
{defaultIcon}
bind:value={space}
on:change={(evt) => {
space = evt.detail
dispatch('change', space)
}}
on:space
on:object
{findDefaultSpace}
/>

View File

@ -692,6 +692,7 @@ input.search {
.min-w-60 { min-width: 15rem; }
.min-w-80 { min-width: 20rem; }
.min-w-100 { min-width: 25rem; }
.min-w-144 { min-width: 25rem; }
.min-w-168 { min-width: 42rem; }
.min-w-min { min-width: min-content; }
.min-h-0 { min-height: 0; }
@ -841,6 +842,7 @@ a.no-line {
.uppercase { text-transform: uppercase; }
.lower { text-transform: lowercase; }
.text-left { text-align: left; }
.text-right { text-align: right !important; }
.text-center { text-align: center; }
.leading-16px { line-height: 16px; }
.leading-3 { line-height: .75rem; }

View File

@ -97,7 +97,9 @@
}}
>
<span slot="content" class="overflow-label disabled" class:content-color={selectedItem === undefined}>
{#if Array.isArray(selectedItem)}
{#if $$slots.content}
<slot name="content" />
{:else if Array.isArray(selectedItem)}
{#if selectedItem.length > 0}
{#each selectedItem as seleceted, i}
<span class="step-row">{seleceted.label}</span>

View File

@ -107,7 +107,7 @@
/>
</div>
{/if}
<div class="scroll">
<div class="scroll" class:mt-2={!enableSearch}>
<div class="box">
<ListView bind:this={list} count={objects.length} bind:selection>
<svelte:fragment slot="item" let:item={idx}>

View File

@ -147,7 +147,7 @@
class:flex-grow={fullSize}
class:w-full={focusable || fullSize}
class:uppercase
on:click={() => {
on:click|stopPropagation={() => {
input.focus()
}}
use:resizeObserver={(element) => {
@ -181,7 +181,9 @@
on:change
on:keydown
on:keypress
on:blur
on:blur={() => {
dispatch('blur', value)
}}
/>
{:else if format === 'number'}
<input
@ -196,7 +198,9 @@
on:change
on:keydown
on:keypress
on:blur
on:blur={() => {
dispatch('blur', value)
}}
/>
{:else}
<input
@ -210,7 +214,9 @@
on:change
on:keydown
on:keypress
on:blur
on:blur={() => {
dispatch('blur', value)
}}
/>
{/if}
</div>

View File

@ -20,7 +20,7 @@
export let label: IntlString
export let params: Record<string, any> = {}
let _value: string | undefined = undefined
let _value: string | undefined
$: if (label !== undefined) {
translate(label, params ?? {}, $themeStore.language)

View File

@ -21,6 +21,7 @@
export let addClass: string | undefined = undefined
export let noScroll: boolean = false
export let kind: 'default' | 'thin' = 'default'
export let updateOnMouse = true
const refs: HTMLElement[] = []
@ -75,10 +76,14 @@
class="list-item{addClass ? ` ${addClass}` : ''}"
class:selection={row === selection}
on:mouseover={mouseAttractor(() => {
onRow(row)
if (updateOnMouse) {
onRow(row)
}
})}
on:mouseenter={mouseAttractor(() => {
onRow(row)
if (updateOnMouse) {
onRow(row)
}
})}
on:focus={() => {}}
bind:this={refs[row]}
@ -100,6 +105,9 @@
margin: 0 0.5rem;
min-width: 0;
border-radius: 0.25rem;
&:hover {
background-color: var(--theme-popup-divider);
}
}
&.thin {
.list-item {

View File

@ -19,10 +19,10 @@
import { closeTooltip, tooltipstore } from '../tooltips'
import type { FadeOptions } from '../types'
import { defaultSP } from '../types'
import IconUpOutline from './icons/UpOutline.svelte'
import { DelayedCaller } from '../utils'
import IconDownOutline from './icons/DownOutline.svelte'
import HalfUpDown from './icons/HalfUpDown.svelte'
import { DelayedCaller } from '../utils'
import IconUpOutline from './icons/UpOutline.svelte'
export let padding: string | undefined = undefined
export let autoscroll: boolean = false

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import type { IntlString, Asset } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, ComponentType } from 'svelte'
import { DateRangeMode } from '@hcengineering/core'
import ui from '../../plugin'
@ -26,7 +26,6 @@
import DPCalendar from './icons/DPCalendar.svelte'
import DPCalendarOver from './icons/DPCalendarOver.svelte'
import { getMonthName } from './internal/DateUtils'
import { ComponentType } from 'svelte'
export let value: number | null | undefined
export let mode: DateRangeMode = DateRangeMode.DATE

View File

@ -0,0 +1,39 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M13 9.99988C13 10.5522 13.4477 10.9999 14 10.9999C14.5523 10.9999 15 10.5522 15 9.99988C15 9.44759 14.5523 8.99988 14 8.99988C13.4477 8.99988 13 9.44759 13 9.99988Z"
fill="#8B97AD"
/>
<path
d="M13 13.9999C13 14.5522 13.4477 14.9999 14 14.9999C14.5523 14.9999 15 14.5522 15 13.9999C15 13.4476 14.5523 12.9999 14 12.9999C13.4477 12.9999 13 13.4476 13 13.9999Z"
fill="#8B97AD"
/>
<path
d="M14 18.9999C13.4477 18.9999 13 18.5522 13 17.9999C13 17.4476 13.4477 16.9999 14 16.9999C14.5523 16.9999 15 17.4476 15 17.9999C15 18.5522 14.5523 18.9999 14 18.9999Z"
fill="#8B97AD"
/>
<path
d="M13 6C13 6.55228 13.4477 7 14 7C14.5523 7 15 6.55228 15 6C15 5.44772 14.5523 5 14 5C13.4477 5 13 5.44772 13 6Z"
fill="#8B97AD"
/>
<path
d="M9 9.99988C9 10.5522 9.44772 10.9999 10 10.9999C10.5523 10.9999 11 10.5522 11 9.99988C11 9.44759 10.5523 8.99988 10 8.99988C9.44772 8.99988 9 9.44759 9 9.99988Z"
fill="#8B97AD"
/>
<path
d="M9 13.9999C9 14.5522 9.44772 14.9999 10 14.9999C10.5523 14.9999 11 14.5522 11 13.9999C11 13.4476 10.5523 12.9999 10 12.9999C9.44772 12.9999 9 13.4476 9 13.9999Z"
fill="#8B97AD"
/>
<path
d="M10 18.9999C9.44772 18.9999 9 18.5522 9 17.9999C9 17.4476 9.44772 16.9999 10 16.9999C10.5523 16.9999 11 17.4476 11 17.9999C11 18.5522 10.5523 18.9999 10 18.9999Z"
fill="#8B97AD"
/>
<path
d="M9 6C9 6.55228 9.44772 7 10 7C10.5523 7 11 6.55228 11 6C11 5.44772 10.5523 5 10 5C9.44772 5 9 5.44772 9 6Z"
fill="#8B97AD"
/>
</svg>

View File

@ -15,9 +15,8 @@
<script lang="ts">
import { getContext } from 'svelte'
import { getMetadata } from '@hcengineering/platform'
import { showPopup } from '../..'
import ui, { showPopup, deviceOptionsStore as deviceInfo } from '../..'
import LangPopup from './LangPopup.svelte'
import ui, { deviceOptionsStore as deviceInfo } from '../..'
let pressed: boolean = false

View File

@ -137,6 +137,7 @@ export { default as IconCalendar } from './components/icons/Calendar.svelte'
export { default as IconFolder } from './components/icons/Folder.svelte'
export { default as IconMoreH } from './components/icons/MoreH.svelte'
export { default as IconMoreV } from './components/icons/MoreV.svelte'
export { default as IconMoreV2 } from './components/icons/MoreV2.svelte'
export { default as IconFile } from './components/icons/File.svelte'
export { default as IconAttachment } from './components/icons/Attachment.svelte'
export { default as IconThread } from './components/icons/Thread.svelte'

View File

@ -13,9 +13,9 @@
// limitations under the License.
//
import justClone from 'just-clone'
import { derived, get, writable } from 'svelte/store'
import { closePopup } from './popups'
import justClone from 'just-clone'
import { type Location as PlatformLocation } from './types'
export function locationToUrl (location: PlatformLocation): string {

View File

@ -13,13 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { Person } from '@hcengineering/contact'
import contact, { Person } from '@hcengineering/contact'
import { Class, Ref, Space } from '@hcengineering/core'
import { SpaceMultiBoxList } from '@hcengineering/presentation'
import { Component, DropdownLabelsIntl } from '@hcengineering/ui'
import attachment from '../plugin'
import { dateFileBrowserFilters, fileTypeFileBrowserFilters } from '..'
import contact from '@hcengineering/contact'
export let requestedSpaceClasses: Ref<Class<Space>>[]
export let spaceId: Ref<Space> | undefined

View File

@ -12,13 +12,23 @@
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ClassSetting } from '@hcengineering/setting-resources'
import { Button, Expandable, Icon, IconAdd, IconDelete, IconEdit, Label, showPopup } from '@hcengineering/ui'
import {
Button,
Expandable,
Icon,
IconAdd,
IconDelete,
IconEdit,
Label,
showPopup,
CheckBox,
DropdownLabelsPopup
} from '@hcengineering/ui'
import bitrix from '../plugin'
import AttributeMapper from './AttributeMapper.svelte'
import FieldMappingPresenter from './FieldMappingPresenter.svelte'
import { CheckBox, DropdownLabelsPopup } from '@hcengineering/ui'
import { deepEqual } from 'fast-equals'
import BitrixFieldLookup from './BitrixFieldLookup.svelte'
import CreateMappingAttribute from './CreateMappingAttribute.svelte'

View File

@ -13,9 +13,17 @@
import core, { Class, Doc, generateId, Ref, Space, WithLookup } from '@hcengineering/core'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import presentation, { getClient, SpaceSelect } from '@hcengineering/presentation'
import { Button, CheckBox, Expandable, Icon, IconAdd, IconClose, Label } from '@hcengineering/ui'
import { DropdownLabels } from '@hcengineering/ui'
import { EditBox } from '@hcengineering/ui'
import {
Button,
CheckBox,
Expandable,
Icon,
IconAdd,
IconClose,
Label,
DropdownLabels,
EditBox
} from '@hcengineering/ui'
import { NumberEditor } from '@hcengineering/view-resources'
import bitrix from '../plugin'
import FieldMappingPresenter from './FieldMappingPresenter.svelte'
@ -136,9 +144,6 @@
_class={core.class.Space}
label={core.string.Space}
bind:value={space}
on:change={(evt) => {
space = evt.detail
}}
autoSelect
spaceQuery={{ _id: { $in: [contact.space.Contacts] } }}
/>

View File

@ -8,8 +8,7 @@
} from '@hcengineering/bitrix'
import { AnyAttribute } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Button, DropdownTextItem, IconAdd, IconDelete } from '@hcengineering/ui'
import { DropdownLabels } from '@hcengineering/ui'
import { Button, DropdownTextItem, IconAdd, IconDelete, DropdownLabels } from '@hcengineering/ui'
import bitrix from '../../plugin'
export let mapping: BitrixEntityMapping

View File

@ -1,10 +1,9 @@
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import { Label } from '@hcengineering/ui'
import { Label, Button, Component, IconBack, IconClose } from '@hcengineering/ui'
import board from '../plugin'
import { createQuery } from '@hcengineering/presentation'
import { MenuPage } from '@hcengineering/board'
import { Button, Component, IconBack, IconClose } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let currentSpace: Ref<Space> | undefined

View File

@ -16,13 +16,12 @@
<script lang="ts">
import { AttachmentDroppable, AttachmentsPresenter } from '@hcengineering/attachment-resources'
import type { Card } from '@hcengineering/board'
import { Employee } from '@hcengineering/contact'
import contact, { Employee } from '@hcengineering/contact'
import type { Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import view from '@hcengineering/view'
import tags from '@hcengineering/tags'
import { getClient } from '@hcengineering/presentation'
import contact from '@hcengineering/contact'
import {
Button,
Component,

View File

@ -1,17 +1,16 @@
<script lang="ts">
import { Icon } from '@hcengineering/ui'
import { Icon, IconSize } from '@hcengineering/ui'
import board from '@hcengineering/board'
export let size: IconSize = 'small'
</script>
<div class="flex-center template-icon">
<Icon icon={board.icon.Board} size="small" />
<Icon icon={board.icon.Board} {size} />
</div>
<style lang="scss">
.template-icon {
width: 100%;
height: 100%;
color: #fff;
background-color: #4474f6;
}
</style>

View File

@ -1,8 +1,7 @@
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import contact, { Employee } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import contact from '@hcengineering/contact'
import { Component } from '@hcengineering/ui'
import board from '../plugin'

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { Label, Status as StatusControl, TextArea } from '@hcengineering/ui'
import { Class, Client, Doc, Ref } from '@hcengineering/core'
import { Class, Client, Doc, Ref, generateId, AttachedData } from '@hcengineering/core'
import { getResource, OK, Resource, Status } from '@hcengineering/platform'
import { Card as Popup, getClient } from '@hcengineering/presentation'
import { Card } from '@hcengineering/board'
@ -10,7 +10,6 @@
import SpaceSelect from '../selectors/SpaceSelect.svelte'
import StateSelect from '../selectors/StateSelect.svelte'
import RankSelect from '../selectors/RankSelect.svelte'
import { generateId, AttachedData } from '@hcengineering/core'
import task from '@hcengineering/task'
export let value: Card

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Board, Card } from '@hcengineering/board'
import { Ref } from '@hcengineering/core'
import { Card } from '@hcengineering/board'
import { Ref, Space } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { DropdownLabels, DropdownTextItem, themeStore } from '@hcengineering/ui'
@ -8,7 +8,7 @@
export let object: Card
export let label: IntlString
export let selected: Ref<Board>
export let selected: Ref<Space>
let spaces: DropdownTextItem[] = []
const spacesQuery = createQuery()

View File

@ -14,9 +14,13 @@
// limitations under the License.
//
import { type Resources } from '@hcengineering/platform'
import { type TodoItem } from '@hcengineering/task'
import { getClient } from '@hcengineering/presentation'
import task, { type Project, type TodoItem } from '@hcengineering/task'
import { type Ref } from '@hcengineering/core'
import Archive from './components/Archive.svelte'
import BoardHeader from './components/BoardHeader.svelte'
import BoardMenu from './components/BoardMenu.svelte'
import BoardPresenter from './components/BoardPresenter.svelte'
import CardPresenter from './components/CardPresenter.svelte'
import CreateBoard from './components/CreateBoard.svelte'
@ -25,29 +29,31 @@ import EditCard from './components/EditCard.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import KanbanView from './components/KanbanView.svelte'
import LabelsView from './components/LabelsView.svelte'
import MoveCard from './components/popups/MoveCard.svelte'
import CopyCard from './components/popups/CopyCard.svelte'
import DateRangePicker from './components/popups/DateRangePicker.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import BoardHeader from './components/BoardHeader.svelte'
import BoardMenu from './components/BoardMenu.svelte'
import MenuMainPage from './components/MenuMainPage.svelte'
import Archive from './components/Archive.svelte'
import TableView from './components/TableView.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import UserBoxList from './components/UserBoxList.svelte'
import CardCoverEditor from './components/editor/CardCoverEditor.svelte'
import CardCoverPresenter from './components/presenters/CardCoverPresenter.svelte'
import CardCoverPicker from './components/popups/CardCoverPicker.svelte'
import CopyCard from './components/popups/CopyCard.svelte'
import DateRangePicker from './components/popups/DateRangePicker.svelte'
import MoveCard from './components/popups/MoveCard.svelte'
import CardCoverPresenter from './components/presenters/CardCoverPresenter.svelte'
import { createCard, getCardFromTodoItem } from './utils/CardUtils'
async function ConvertToCard (object: TodoItem): Promise<void> {
const client = getClient()
const todoItemCard = await getCardFromTodoItem(client, object)
if (todoItemCard === undefined) return
// TODO: Add filtering if requierd, or pass a type from UI
const project = await client.findOne(task.class.Project, { _id: object.space as Ref<Project> })
const taskTypes = await client.findAll(task.class.TaskType, { parent: project?.type })
await createCard(client, todoItemCard.space, todoItemCard.status, {
title: object.name,
assignee: object.assignee,
dueDate: object.dueTo
dueDate: object.dueTo,
kind: taskTypes[0]._id
})
await client.remove(object)

View File

@ -18,7 +18,7 @@ export async function createCard (
client: Client,
space: Ref<Space>,
status: Ref<Status>,
attribues: Partial<AttachedData<Card>>
attribues: Partial<AttachedData<Card>> & { kind: Card['kind'] }
): Promise<Ref<Card>> {
const sequence = await client.findOne(task.class.Sequence, { attachedTo: board.class.Card })
if (sequence === undefined) {

View File

@ -20,7 +20,7 @@ import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import { TagCategory } from '@hcengineering/tags'
import type { Project, ProjectTypeCategory, Task } from '@hcengineering/task'
import type { Project, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui'
import { Action, ActionCategory } from '@hcengineering/view'
@ -91,8 +91,13 @@ const boards = plugin(boardId, {
},
category: {
Card: '' as Ref<ActionCategory>,
Other: '' as Ref<TagCategory>,
BoardType: '' as Ref<ProjectTypeCategory>
Other: '' as Ref<TagCategory>
},
descriptors: {
BoardType: '' as Ref<ProjectTypeDescriptor>
},
taskType: {
Card: '' as Ref<TaskType>
},
attribute: {
State: '' as Ref<Attribute<Status>>

View File

@ -15,9 +15,8 @@
<script lang="ts">
import { Message } from '@hcengineering/chunter'
import { Person } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import { personByIdStore, Avatar } from '@hcengineering/contact-resources'
import { Doc, IdMap, Ref } from '@hcengineering/core'
import { Avatar } from '@hcengineering/contact-resources'
import { Label, TimeSince } from '@hcengineering/ui'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { DocUpdates } from '@hcengineering/notification'

View File

@ -16,11 +16,10 @@
import type { Comment } from '@hcengineering/chunter'
import type { AttachedData, TxCreateDoc } from '@hcengineering/core'
import { getClient, MessageViewer } from '@hcengineering/presentation'
import { AttachmentDocList } from '@hcengineering/attachment-resources'
import { AttachmentDocList, AttachmentRefInput } from '@hcengineering/attachment-resources'
import { Button } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import chunter from '../../plugin'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { LinkPresenter } from '@hcengineering/view-resources'
export let tx: TxCreateDoc<Comment>

View File

@ -13,6 +13,7 @@
// limitations under the License.
//
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
import type { Person } from '@hcengineering/contact'
import type {
Account,
@ -29,8 +30,7 @@ import { NotificationType } from '@hcengineering/notification'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import { AnyComponent, ResolvedLocation } from '@hcengineering/ui'
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import { Action } from '@hcengineering/view'
/**

View File

@ -24,7 +24,7 @@
export let label: IntlString
export let value: Ref<Account>[]
export let onChange: (refs: Ref<Account>[]) => void
export let onChange: ((refs: Ref<Account>[]) => void) | undefined
export let readonly = false
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
@ -41,7 +41,7 @@
}
update = async () => {
const accounts = await client.findAll(contact.class.PersonAccount, { person: { $in: evt.detail } })
onChange(accounts.map((it) => it._id))
onChange?.(accounts.map((it) => it._id))
if (timer !== null) {
clearTimeout(timer)
}
@ -52,7 +52,7 @@
}
onDestroy(() => {
update?.()
void update?.()
})
const excludedQuery = createQuery()

View File

@ -16,8 +16,9 @@
import type { IntlString } from '@hcengineering/platform'
import { translate } from '@hcengineering/platform'
import { copyTextToClipboard } from '@hcengineering/presentation'
import { PopupOptions, themeStore } from '@hcengineering/ui'
import {
PopupOptions,
themeStore,
Button,
createFocusManager,
FocusHandler,

View File

@ -16,10 +16,9 @@
<script lang="ts">
import type { AttachedData, Class, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ButtonKind, ButtonSize, closeTooltip } from '@hcengineering/ui'
import { ButtonKind, ButtonSize, closeTooltip, showPopup } from '@hcengineering/ui'
import { Channel, ChannelProvider } from '@hcengineering/contact'
import { showPopup } from '@hcengineering/ui'
import contact from '../plugin'
import ChannelsDropdown from './ChannelsDropdown.svelte'

View File

@ -69,7 +69,7 @@
{allowDeselect}
{titleDeselect}
{placeholder}
{docQuery}
docQuery={readonly ? { ...docQuery, _id: { $in: selectedUsers } } : docQuery}
{filter}
groupBy={'_class'}
bind:selectedObjects={selectedUsers}

View File

@ -194,7 +194,8 @@ export const contactPlugin = plugin(contactId, {
UserBoxList: '' as AnyComponent,
ChannelPresenter: '' as AnyComponent,
SpaceMembers: '' as AnyComponent,
DeleteConfirmationPopup: '' as AnyComponent
DeleteConfirmationPopup: '' as AnyComponent,
AccountArrayEditor: '' as AnyComponent
},
channelProvider: {
Email: '' as Ref<ChannelProvider>,

View File

@ -13,8 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import contact, { Employee } from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, Ref } from '@hcengineering/core'
import { tzDateCompare, type Department, type Request, type RequestType, type Staff } from '@hcengineering/hr'
import {

View File

@ -14,8 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachedData, AttachedDoc, Doc, Ref } from '@hcengineering/core'
import { generateId } from '@hcengineering/core'
import { AttachedData, AttachedDoc, Doc, Ref, generateId } from '@hcengineering/core'
import { OK, Status } from '@hcengineering/platform'
import { Card, getClient } from '@hcengineering/presentation'
import type { Category } from '@hcengineering/inventory'

View File

@ -1,17 +1,16 @@
<script lang="ts">
import { Icon } from '@hcengineering/ui'
import lead from '@hcengineering/lead'
import { Icon, IconSize } from '@hcengineering/ui'
export let size: IconSize = 'small'
</script>
<div class="flex-center template-icon">
<Icon icon={lead.icon.LeadApplication} size="small" />
<Icon icon={lead.icon.LeadApplication} {size} />
</div>
<style lang="scss">
.template-icon {
width: 100%;
height: 100%;
color: #fff;
background-color: #4474f6;
}
</style>

View File

@ -19,7 +19,7 @@ import type { Attribute, Class, Doc, Ref, Status, Timestamp } from '@hcengineeri
import { Mixin } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Project, ProjectTypeCategory, Task } from '@hcengineering/task'
import type { Project, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
/**
* @public
@ -82,8 +82,11 @@ const lead = plugin(leadId, {
LeadApplication: '' as Asset,
CreateCustomer: '' as Asset
},
category: {
FunnelTypeCategory: '' as Ref<ProjectTypeCategory>
descriptors: {
FunnelType: '' as Ref<ProjectTypeDescriptor>
},
taskType: {
Lead: '' as Ref<TaskType>
}
})

View File

@ -20,13 +20,12 @@
StylishEdit,
Label,
Button,
deviceOptionsStore as deviceInfo
deviceOptionsStore as deviceInfo,
themeStore
} from '@hcengineering/ui'
import StatusControl from './StatusControl.svelte'
import { OK, Status, Severity } from '@hcengineering/platform'
import { OK, Status, Severity, translate } from '@hcengineering/platform'
import type { IntlString } from '@hcengineering/platform'
import { translate } from '@hcengineering/platform'
import { themeStore } from '@hcengineering/ui'
import login from '../plugin'
import { onMount } from 'svelte'

View File

@ -14,11 +14,10 @@
// limitations under the License.
-->
<script lang="ts">
import { Popup, Scroller, deviceOptionsStore as deviceInfo, location, ticker } from '@hcengineering/ui'
import { Popup, Scroller, deviceOptionsStore as deviceInfo, location, ticker, themeStore } from '@hcengineering/ui'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { themeStore } from '@hcengineering/ui'
import workbench from '@hcengineering/workbench'
import { onDestroy } from 'svelte'
import Confirmation from './Confirmation.svelte'

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import chunter, { getDirectChannel } from '@hcengineering/chunter'
import { Employee, PersonAccount } from '@hcengineering/contact'
import contact, { Employee, PersonAccount } from '@hcengineering/contact'
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { DocUpdates } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
@ -32,7 +32,6 @@
Separator
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import contact from '@hcengineering/contact'
import { UsersPopup } from '@hcengineering/contact-resources'
import notification from '../plugin'

View File

@ -14,15 +14,16 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import type {
NotificationGroup,
NotificationPreferencesGroup,
NotificationSetting,
NotificationType
} from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Label, Scroller, Separator, defineSeparators, settingsSeparators } from '@hcengineering/ui'
import { Location, Scroller, getCurrentResolvedLocation, navigate, resolvedLocationStore } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import notification from '../plugin'
import GroupElement from './GroupElement.svelte'
import NotificationGroupSetting from './NotificationGroupSetting.svelte'
@ -31,7 +32,7 @@
let groups: NotificationGroup[] = []
let preferencesGroups: NotificationPreferencesGroup[] = []
client.findAll(notification.class.NotificationGroup, {}).then((res) => {
void client.findAll(notification.class.NotificationGroup, {}).then((res) => {
groups = res
})
@ -52,7 +53,7 @@
let group: Ref<NotificationGroup> | undefined = undefined
let currentPreferenceGroup: NotificationPreferencesGroup | undefined = undefined
client.findAll(notification.class.NotificationPreferencesGroup, {}).then((res) => {
void client.findAll(notification.class.NotificationPreferencesGroup, {}).then((res) => {
preferencesGroups = res
})
@ -64,15 +65,19 @@
}
}
defineSeparators('settingNotify', settingsSeparators)
onDestroy(
resolvedLocationStore.subscribe((loc) => {
void (async (loc: Location): Promise<void> => {
group = loc.path[4] as Ref<NotificationGroup>
currentPreferenceGroup = undefined
})(loc)
})
)
</script>
<div class="flex h-full clear-mins">
<div class="antiPanel-navigator">
<div class="flex">
<div class="antiPanel-element ml-4 mt-2">
<div class="antiPanel-wrap__content">
<div class="antiNav-header overflow-label">
<Label label={notification.string.Notifications} />
</div>
<Scroller shrink>
{#each preferencesGroups as preferenceGroup}
<GroupElement
@ -82,6 +87,9 @@
on:click={() => {
currentPreferenceGroup = preferenceGroup
group = undefined
const loc = getCurrentResolvedLocation()
loc.path.length = 4
navigate(loc)
}}
/>
{/each}
@ -96,6 +104,10 @@
on:click={() => {
group = gr._id
currentPreferenceGroup = undefined
const loc = getCurrentResolvedLocation()
loc.path[4] = group
loc.path.length = 5
navigate(loc)
}}
/>
{/each}
@ -103,8 +115,6 @@
</Scroller>
</div>
</div>
<Separator name={'settingNotify'} index={0} color={'var(--theme-navpanel-border)'} />
<div class="antiPanel-component filled">
{#if group}
<NotificationGroupSetting {group} {settings} />

View File

@ -15,13 +15,12 @@
<script lang="ts">
import attachment from '@hcengineering/attachment'
import contact, { Channel, getName, Person } from '@hcengineering/contact'
import { ChannelsEditor } from '@hcengineering/contact-resources'
import { ChannelsEditor, Avatar } from '@hcengineering/contact-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Avatar } from '@hcengineering/contact-resources'
import { Component, Label } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import recruit from '../plugin'
import notification from '@hcengineering/notification'
import chunter from '@hcengineering/chunter'
export let candidate: Person | undefined
export let disabled: boolean = false
@ -64,7 +63,7 @@
<div class="footer">
<div class="flex-row-center gap-2">
<Component
is={notification.component.ChatMessagesPresenter}
is={chunter.component.ChatMessagesPresenter}
props={{ value: candidate, size: 'small', showCounter: true }}
/>
<Component

View File

@ -41,7 +41,7 @@
getClient
} from '@hcengineering/presentation'
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task, { calcRank, getStates } from '@hcengineering/task'
import task, { TaskType, calcRank, getStates } from '@hcengineering/task'
import ui, {
Button,
ColorPopup,
@ -62,7 +62,7 @@
import CandidateCard from './CandidateCard.svelte'
import VacancyCard from './VacancyCard.svelte'
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
import { typeStore } from '@hcengineering/task-resources'
import { TaskKindSelector, selectedTypeStore, typeStore } from '@hcengineering/task-resources'
export let space: Ref<Vacancy>
export let candidate: Ref<Candidate>
@ -94,7 +94,9 @@
modifiedOn: Date.now(),
modifiedBy: '' as Ref<Account>,
startDate: null,
dueDate: null
dueDate: null,
kind: '' as Ref<TaskType>,
isDone: false
}
const dispatch = createEventDispatcher()
@ -106,7 +108,7 @@
return (preserveCandidate || _candidate === undefined) && assignee === undefined
}
async function createApplication () {
async function createApplication (): Promise<void> {
if (selectedState === undefined) {
throw new Error(`Please select initial state:${_space}`)
}
@ -114,6 +116,9 @@
if (sequence === undefined) {
throw new Error('sequence object not found')
}
if (kind === undefined) {
throw new Error('kind is not specified')
}
const lastOne = await client.findOne(recruit.class.Applicant, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
@ -145,7 +150,8 @@
assignee: doc.assignee,
rank: calcRank(lastOne, undefined),
startDate: null,
dueDate: null
dueDate: null,
kind
},
doc._id
)
@ -209,6 +215,8 @@
return { id: s._id, label: s.name, color: s.color ?? getColorNumberByText(s.name) }
})
let kind: Ref<TaskType> | undefined
const manager = createFocusManager()
const existingApplicationsQuery = createQuery()
@ -268,6 +276,7 @@
<svelte:fragment slot="title">
<div class="flex-row-center gap-2">
<Label label={recruit.string.CreateApplication} />
<TaskKindSelector projectType={vacancy?.type} bind:taskType={kind} baseClass={recruit.class.Applicant} />
</div>
</svelte:fragment>
<StatusControl slot="error" {status} />
@ -300,7 +309,7 @@
<div class="flex-grow">
<SpaceSelect
_class={recruit.class.Vacancy}
spaceQuery={{ archived: false }}
spaceQuery={{ archived: false, ...($selectedTypeStore !== undefined ? { type: $selectedTypeStore } : {}) }}
spaceOptions={orgOptions}
readonly={preserveVacancy}
label={recruit.string.Vacancy}
@ -309,9 +318,6 @@
label: recruit.string.CreateVacancy
}}
bind:value={_space}
on:change={(evt) => {
_space = evt.detail
}}
component={VacancyOrgPresenter}
componentProps={{ inline: true }}
>

View File

@ -35,12 +35,16 @@
import recruit from '../plugin'
import Company from './icons/Company.svelte'
import Vacancy from './icons/Vacancy.svelte'
import { selectedTypeStore, typeStore } from '@hcengineering/task-resources'
const dispatch = createEventDispatcher()
let name: string = ''
let template: ProjectType | undefined
let typeId: Ref<ProjectType> | undefined
let typeId: Ref<ProjectType> | undefined = $selectedTypeStore
$: typeType = typeId !== undefined ? $typeStore.get(typeId) : undefined
let appliedTemplateId: Ref<ProjectType> | undefined
let objectId: Ref<VacancyClass> = generateId()
let issueTemplates: FindResult<IssueTemplate>
@ -144,8 +148,8 @@
return resId
}
async function createVacancy () {
if (typeId === undefined) {
async function createVacancy (): Promise<void> {
if (typeId === undefined || typeType === undefined) {
throw Error(`Failed to find target project type: ${typeId}`)
}
@ -184,6 +188,10 @@
}
await descriptionBox.createAttachments()
// Add vacancy mixin
await client.createMixin(objectId, recruit.class.Vacancy, core.space.Space, typeType.targetClass, {})
objectId = generateId()
dispatch('close', id)
@ -226,6 +234,7 @@
}}
on:changeContent
>
{typeType?.name}
<div class="flex-row-center clear-mins">
<div class="mr-3">
<Button focusIndex={1} icon={Vacancy} size={'medium'} kind={'link-bordered'} noFocus />
@ -275,7 +284,7 @@
<Component
is={task.component.ProjectTypeSelector}
props={{
categories: [recruit.category.VacancyTypeCategories],
descriptors: [recruit.descriptors.VacancyType],
type: typeId,
focusIndex: 4,
kind: 'regular',

View File

@ -26,7 +26,7 @@
import { Component, DueDatePresenter } from '@hcengineering/ui'
import { BuildModelKey } from '@hcengineering/view'
import { DocNavLink, ObjectPresenter, enabledConfig, statusStore } from '@hcengineering/view-resources'
import { ChatMessagesPresenter } from '@hcengineering/notification-resources'
import { ChatMessagesPresenter } from '@hcengineering/chunter-resources'
import ApplicationPresenter from './ApplicationPresenter.svelte'
@ -135,13 +135,15 @@
{/if}
{#if enabledConfig(config, 'comments')}
<ChatMessagesPresenter value={object.comments} {object} kind="list" size="x-small" />
<ChatMessagesPresenter
value={object.$lookup?.attachedTo?.comments}
object={object.$lookup?.attachedTo}
withInput={false}
kind="list"
size="x-small"
/>
{#if object.$lookup?.attachedTo}
<ChatMessagesPresenter
value={object.$lookup?.attachedTo?.comments}
object={object.$lookup?.attachedTo}
withInput={false}
kind="list"
size="x-small"
/>
{/if}
{/if}
</div>
{#if enabledConfig(config, 'assignee')}

View File

@ -172,9 +172,6 @@
label: recruit.string.CreateVacancy
}}
bind:value={_space}
on:change={(evt) => {
_space = evt.detail
}}
component={VacancyOrgPresenter}
componentProps={{ inline: true }}
>

View File

@ -155,9 +155,6 @@
label: recruit.string.CreateVacancy
}}
bind:value={_space}
on:change={(evt) => {
_space = evt.detail
}}
component={VacancyOrgPresenter}
componentProps={{ inline: true }}
>

View File

@ -17,8 +17,7 @@
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Card, getClient } from '@hcengineering/presentation'
import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
import { Button, CheckBox, EditBox, Lazy, ListView, Loading } from '@hcengineering/ui'
import { Expandable } from '@hcengineering/ui'
import { Button, CheckBox, EditBox, Lazy, ListView, Loading, Expandable } from '@hcengineering/ui'
import { FILTER_DEBOUNCE_MS } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'

View File

@ -1,16 +1,16 @@
<script lang="ts">
import { IconSize } from '@hcengineering/ui'
import VacancyIcon from './icons/Vacancy.svelte'
export let size: IconSize = 'small'
</script>
<div class="flex-center template-icon">
<VacancyIcon size="small" />
<VacancyIcon {size} />
</div>
<style lang="scss">
.template-icon {
width: 100%;
height: 100%;
color: #fff;
background-color: #60b96e;
}
</style>

View File

@ -23,7 +23,7 @@
import { NavLink } from '@hcengineering/view-resources'
import recruit from '../plugin'
import VacancyIcon from './icons/Vacancy.svelte'
import notification from '@hcengineering/notification'
import chunter from '@hcengineering/chunter'
export let vacancy: WithLookup<Vacancy> | undefined
export let disabled: boolean = false
@ -97,7 +97,7 @@
<div class="footer">
<div class="flex-row-center gap-2">
<Component
is={notification.component.ChatMessagesPresenter}
is={chunter.component.ChatMessagesPresenter}
props={{ value: vacancy, size: 'small', showCounter: true }}
/>
<Component

View File

@ -17,7 +17,7 @@
import { ProjectType } from '@hcengineering/task'
import { StyledTextBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker'
import { Button, Component, EditBox, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { Button, Component, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { getFiltredKeys } from '@hcengineering/view-resources'
import recruit from '../plugin'
@ -29,25 +29,11 @@
const customKeys = getFiltredKeys(hierarchy, type._class, []).filter((key) => key.attr.isCustom)
async function onDescriptionChange (value: string) {
await client.update(type, { description: value })
}
async function onShortDescriptionChange (value: string) {
await client.update(type, { shortDescription: value })
await client.diffUpdate(type, { description: value })
}
</script>
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={recruit.string.Description} />
</div>
<div class="mt-3">
<EditBox
kind={'small-style'}
bind:value={type.shortDescription}
on:change={() => onShortDescriptionChange(type.shortDescription ?? '')}
/>
</div>
<div class="mt-9">
<div class="mt-4">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={recruit.string.FullDescription} />
</div>
@ -56,6 +42,7 @@
<StyledTextBox
kind={'emphasized'}
alwaysEdit
maxHeight={'card'}
showButtons={false}
content={type.description ?? ''}
on:value={(evt) => onDescriptionChange(evt.detail)}

View File

@ -14,17 +14,17 @@
//
import {
toIdMap,
type Client,
type Doc,
type DocumentQuery,
type FindResult,
type ObjQueryType,
type Ref,
type RelatedDocument,
toIdMap
type RelatedDocument
} from '@hcengineering/core'
import { OK, type Resources, Severity, Status } from '@hcengineering/platform'
import { type ObjectSearchResult, createQuery } from '@hcengineering/presentation'
import { OK, Severity, Status, type Resources } from '@hcengineering/platform'
import { createQuery, type ObjectSearchResult } from '@hcengineering/presentation'
import { type Applicant, type Candidate, type Vacancy } from '@hcengineering/recruit'
import task from '@hcengineering/task'
import { showPopup } from '@hcengineering/ui'
@ -49,13 +49,13 @@ import SkillsView from './components/SkillsView.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import Vacancies from './components/Vacancies.svelte'
import VacancyCountPresenter from './components/VacancyCountPresenter.svelte'
import VacancyEditor from './components/VacancyEditor.svelte'
import VacancyItem from './components/VacancyItem.svelte'
import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
import VacancyList from './components/VacancyList.svelte'
import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte'
import VacancyPresenter from './components/VacancyPresenter.svelte'
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
import VacancyEditor from './components/VacancyEditor.svelte'
import CreateOpinion from './components/review/CreateOpinion.svelte'
import CreateReview from './components/review/CreateReview.svelte'
import EditReview from './components/review/EditReview.svelte'
@ -78,8 +78,8 @@ import {
resolveLocation
} from './utils'
import { MoveApplicant } from './actionImpl'
import { get } from 'svelte/store'
import { MoveApplicant } from './actionImpl'
async function createOpinion (object: Doc): Promise<void> {
showPopup(CreateOpinion, { space: object.space, review: object._id })

View File

@ -30,7 +30,7 @@ import type {
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { TagReference } from '@hcengineering/tags'
import type { Project, ProjectTypeCategory, Task } from '@hcengineering/task'
import type { Project, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
import { AnyComponent, ResolvedLocation } from '@hcengineering/ui'
/**
@ -162,8 +162,8 @@ const recruit = plugin(recruitId, {
Review: '' as Ref<Class<Review>>,
Opinion: '' as Ref<Class<Opinion>>
},
category: {
VacancyTypeCategories: '' as Ref<ProjectTypeCategory>
descriptors: {
VacancyType: '' as Ref<ProjectTypeDescriptor>
},
mixin: {
Candidate: '' as Ref<Mixin<Candidate>>,
@ -200,6 +200,9 @@ const recruit = plugin(recruitId, {
},
space: {
Reviews: '' as Ref<Calendar>
},
taskTypes: {
Applicant: '' as Ref<TaskType>
}
})

View File

@ -1,7 +1,6 @@
{
"string": {
"Setting": "Setting",
"ManageProjects": "Manage Projects",
"Setting": "Setting",
"Integrations": "Integrations",
"Support": "Support",
"Privacy": "Privacy",

View File

@ -1,7 +1,6 @@
{
"string": {
"Setting": "Настройки",
"ManageProjects": "Управление проектами",
"Integrations": "Интеграции",
"Support": "Поддержка",
"Privacy": "Конфиденциальность",

View File

@ -28,34 +28,29 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="antiNav-element"
class="antiNav-element flex-row-center flex-between"
class:selected
class:expandable
on:click|stopPropagation={() => {
dispatch('click')
}}
>
<div class="an-element__icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
<div class="flex-row-center flex flex-between flex-grow">
<div class="flex-row-center">
<div class="an-element__icon">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
</div>
<span class="an-element__label" class:trans-title={expandable}>
{#if label}<Label {label} />{/if}
</span>
</div>
<slot name="tools" />
</div>
<span class="an-element__label">
{#if label}<Label {label} />{/if}
</span>
</div>
<style lang="scss">
.expandable {
position: relative;
&::after {
content: '▶';
position: absolute;
top: 50%;
right: 0.5rem;
font-size: 0.375rem;
color: var(--dark-color);
transform: translateY(-50%);
}
}
</style>

View File

@ -26,33 +26,34 @@
RefTo,
Type
} from '@hcengineering/core'
import { getResource, IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { IntlString, getResource } from '@hcengineering/platform'
import presentation, { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import {
Action,
ActionIcon,
AnySvelteComponent,
CircleButton,
Component,
getEventPositionElement,
Icon,
IconAdd,
IconDelete,
IconEdit,
IconMoreV,
IconMoreV2,
Label,
Menu,
getEventPositionElement,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { getContextActions } from '@hcengineering/view-resources/src/actions'
import { getContextActions } from '@hcengineering/view-resources'
import settings from '../plugin'
import CreateAttribute from './CreateAttribute.svelte'
import EditAttribute from './EditAttribute.svelte'
import EditClassLabel from './EditClassLabel.svelte'
export let _class: Ref<Class<Doc>>
export let ofClass: Ref<Class<Doc>> | undefined
export let ofClass: Ref<Class<Doc>> | undefined = undefined
export let useOfClassAttributes = true
export let showTitle = true
export let showCreate = true
export let attributeMapper:
| {
@ -60,7 +61,7 @@
label: IntlString
props: Record<string, any>
}
| undefined
| undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -77,9 +78,10 @@
function getCustomAttributes (_class: Ref<Class<Doc>>): AnyAttribute[] {
const cl = hierarchy.getClass(_class)
const attributes = Array.from(
hierarchy.getAllAttributes(_class, _class === ofClass ? core.class.Doc : cl.extends).values()
hierarchy
.getAllAttributes(_class, _class === ofClass && useOfClassAttributes ? core.class.Doc : cl.extends)
.values()
)
// const filtred = attributes.filter((p) => !p.hidden)
return attributes
}
@ -89,19 +91,19 @@
attributes = getCustomAttributes(_class)
})
function update () {
function update (): void {
attributes = getCustomAttributes(_class)
}
function createAttribute () {
export function createAttribute (): void {
showPopup(CreateAttribute, { _class }, 'top', update)
}
async function editAttribute (attribute: AnyAttribute, exist: boolean): Promise<void> {
export async function editAttribute (attribute: AnyAttribute, exist: boolean): Promise<void> {
showPopup(EditAttribute, { attribute, exist }, 'top', update)
}
async function removeAttribute (attribute: AnyAttribute, exist: boolean): Promise<void> {
export async function removeAttribute (attribute: AnyAttribute, exist: boolean): Promise<void> {
showPopup(
MessageBox,
{
@ -110,7 +112,7 @@
},
'top',
async (result) => {
if (result) {
if (result != null) {
await client.remove(attribute)
update()
}
@ -118,7 +120,7 @@
)
}
async function showMenu (ev: MouseEvent, attribute: AnyAttribute) {
async function showMenu (ev: MouseEvent, attribute: AnyAttribute): Promise<void> {
const exist = (await client.findOne(attribute.attributeOf, { [attribute.name]: { $exists: true } })) !== undefined
const actions: Action[] = [
@ -126,16 +128,16 @@
label: presentation.string.Edit,
icon: IconEdit,
action: async () => {
editAttribute(attribute, exist)
await editAttribute(attribute, exist)
}
}
]
if (attribute.isCustom) {
if (attribute.isCustom === true) {
actions.push({
label: presentation.string.Remove,
icon: IconDelete,
action: async () => {
removeAttribute(attribute, exist)
await removeAttribute(attribute, exist)
}
})
}
@ -146,7 +148,7 @@
icon: it.icon,
action: async (_: any, evt: Event) => {
const r = await getResource(it.action)
r(attribute, evt, it.actionProps)
await r(attribute, evt, it.actionProps)
}
}))
)
@ -176,106 +178,105 @@
}
</script>
<div class="flex-row-center fs-title mb-3">
{#if clazz?.icon}
<div class="mr-2 flex">
<Icon icon={clazz.icon} size={'medium'} />
{#if clazz.kind === ClassifierKind.MIXIN && hierarchy.hasMixin(clazz, settings.mixin.UserMixin)}
<Icon icon={IconAdd} size={'x-small'} />
{/if}
</div>
{/if}
{#if clazz}
<Label label={clazz.label} />
{#if clazz.kind === ClassifierKind.MIXIN && hierarchy.hasMixin(clazz, settings.mixin.UserMixin)}
<div class="ml-2">
<ActionIcon icon={IconEdit} size="small" action={editLabel} />
{#if showTitle}
<div class="flex-row-center fs-title mb-3">
{#if clazz?.icon}
<div class="mr-2 flex">
<Icon icon={clazz.icon} size={'medium'} />
{#if clazz.kind === ClassifierKind.MIXIN && hierarchy.hasMixin(clazz, settings.mixin.UserMixin)}
<Icon icon={IconAdd} size={'x-small'} />
{/if}
</div>
{/if}
{/if}
</div>
<div class="flex-between trans-title mb-3">
<Label label={settings.string.Attributes} />
<CircleButton icon={IconAdd} size="medium" on:click={createAttribute} />
</div>
<table class="antiTable">
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<!-- <th>Field name</th> -->
<th>
<div class="antiTable-cells">
<Label label={settings.string.Attribute} />
{#if clazz}
<Label label={clazz.label} />
{#if clazz.kind === ClassifierKind.MIXIN && hierarchy.hasMixin(clazz, settings.mixin.UserMixin)}
<div class="ml-2">
<ActionIcon icon={IconEdit} size="small" action={editLabel} />
</div>
</th>
<th>
<div class="antiTable-cells">
<Label label={settings.string.Type} />
</div>
</th>
<th>
<div class="antiTable-cells">
<Label label={settings.string.Visibility} />
</div>
</th>
<th>
<div class="antiTable-cells">
<Label label={settings.string.Custom} />
</div>
</th>
{#if attributeMapper}
<th>
<Label label={attributeMapper.label} />
</th>
{/if}
</tr>
</thead>
<tbody>
{#each attributes as attr}
{@const attrType = getAttrType(attr.type)}
<tr
class="antiTable-body__row"
on:contextmenu={(ev) => {
ev.preventDefault()
showMenu(ev, attr)
}}
>
<td>
<div class="antiTable-cells__firstCell whitespace-nowrap">
<Label label={attr.label} />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div id="context-menu" class="antiTable-cells__firstCell-menuRow" on:click={(ev) => showMenu(ev, attr)}>
<IconMoreV size={'small'} />
</div>
{/if}
</div>
{/if}
{#if showCreate}
<div class="flex-between trans-title mb-3">
<Label label={settings.string.Attributes} />
<CircleButton icon={IconAdd} size="medium" on:click={createAttribute} />
</div>
{/if}
{#each attributes as attr, i}
{@const attrType = getAttrType(attr.type)}
<tr
class="antiTable-body__row"
on:contextmenu={(ev) => {
ev.preventDefault()
void showMenu(ev, attr)
}}
>
<td>
{#if i === 0 && clazz?.label !== undefined}
<div class="trans-title">
<Label label={clazz.label} />
</div>
{/if}
</td>
<td>
<div class="antiTable-cells__firstCell whitespace-nowrap flex-row-center">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div id="context-menu" on:click={(ev) => showMenu(ev, attr)}>
<div class="p-1">
<IconMoreV2 size={'medium'} />
</div>
</div>
{#if attr.icon !== undefined}
<div class="p-1">
<Icon icon={attr.icon} size={'small'} />
</div>
</td>
<td class="select-text whitespace-nowrap">
<Label label={attr.type.label} />
{#if attrType !== undefined}
: <Label label={attrType} />
{/if}
{#if attr.type._class === core.class.EnumOf}
{#await getEnumName(attr.type) then name}
{#if name}
: {name}
{/if}
{/await}
{/if}
</td>
<td>
{#if attr.hidden}
<Label label={settings.string.Hidden} />
{/if}
</td>
<td>
<Component is={view.component.BooleanTruePresenter} props={{ value: attr.isCustom ?? false }} />
</td>
{#if attributeMapper}
<td>
<svelte:component this={attributeMapper.component} {...attributeMapper.props} attribute={attr} />
</td>
{/if}
</tr>
{/each}
</tbody>
</table>
{#if attr.isCustom}
<div class="trans-title p-1">
<Label label={settings.string.Custom} />
</div>
{/if}
<div class:accent={!attr.hidden}>
<Label label={attr.label} />
</div>
</div>
</td>
<td class="select-text whitespace-nowrap trans-title text-xs text-right" style:padding-right={'1rem !important'}>
<Label label={attr.type.label} />
{#if attrType !== undefined}
: <Label label={attrType} />
{/if}
{#if attr.type._class === core.class.EnumOf}
{#await getEnumName(attr.type) then name}
{#if name}
: {name}
{/if}
{/await}
{/if}
</td>
{#if attributeMapper}
<td>
<svelte:component this={attributeMapper.component} {...attributeMapper.props} attribute={attr} />
</td>
{/if}
</tr>
{/each}
{#if attributes.length === 0}
<tr class="antiTable-body__row">
<td>
<div class="trans-title">
{#if clazz}
<Label label={clazz.label} />
{/if}
</div>
</td>
<td class="select-text whitespace-nowrap"> </td>
<td> </td>
{#if attributeMapper}
<td> </td>
{/if}
</tr>
{/if}

View File

@ -15,8 +15,9 @@
<script lang="ts">
import { Class, ClassifierKind, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getEventPositionElement, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { Icon, IconAdd, getEventPositionElement, showPopup } from '@hcengineering/ui'
import { ContextMenu } from '@hcengineering/view-resources'
import ObjectPresenter from '@hcengineering/view-resources/src/components/ObjectPresenter.svelte'
import { createEventDispatcher } from 'svelte'
import settings from '../plugin'
@ -25,10 +26,10 @@
export let ofClass: Ref<Class<Doc>> | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
function getDescendants (_class: Ref<Class<Doc>>): Ref<Class<Doc>>[] {
const hierarchy = client.getHierarchy()
const result: Ref<Class<Doc>>[] = []
const desc = hierarchy.getDescendants(_class)
const vars = [ClassifierKind.MIXIN]
@ -55,7 +56,7 @@
</script>
{#each classes as cl}
{@const clazz = hierarchy.getClass(cl)}
{@const clazz = client.getHierarchy().getClass(cl)}
{@const desc = getDescendants(cl)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@ -69,16 +70,18 @@
showContextMenu(evt, clazz)
}}
>
<div class="flex gap-2">
<div class="flex-row-center">
{#if clazz.icon}
<div class="mr-2 flex">
<div class="mr-1 flex">
<Icon icon={clazz.icon} size={'medium'} />
{#if clazz.kind === ClassifierKind.MIXIN && hierarchy.hasMixin(clazz, settings.mixin.UserMixin)}
{#if clazz.kind === ClassifierKind.MIXIN && client.getHierarchy().hasMixin(clazz, settings.mixin.UserMixin)}
<Icon icon={IconAdd} size={'x-small'} fill={'var(--theme-dark-color)'} />
{/if}
</div>
{/if}
<span class="overflow-label caption-color"><Label label={clazz.label} /></span>
<span class="overflow-label caption-color">
<ObjectPresenter _class={clazz._class} objectId={clazz._id} value={clazz} />
</span>
</div>
</div>
{#if desc.length}

View File

@ -16,21 +16,22 @@
import core, { Class, Doc, Obj, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, getLocation, Icon, Label, navigate } from '@hcengineering/ui'
import { AnySvelteComponent, Icon, Label, getLocation, navigate } from '@hcengineering/ui'
import setting from '../plugin'
import { filterDescendants } from '../utils'
import ClassAttributes from './ClassAttributes.svelte'
import ClassHierarchy from './ClassHierarchy.svelte'
export let ofClass: Ref<Class<Obj>> | undefined
export let ofClass: Ref<Class<Obj>> | undefined = undefined
export let attributeMapper:
| {
component: AnySvelteComponent
label: IntlString
props: Record<string, any>
}
| undefined
| undefined = undefined
export let withoutHeader = false
export let useOfClassAttributes = true
const loc = getLocation()
const client = getClient()
@ -47,26 +48,38 @@
const clQuery = createQuery()
let classes: Ref<Class<Doc>>[] = []
clQuery.query(core.class.Class, {}, (res) => {
classes = filterDescendants(hierarchy, ofClass, res)
let rawClasses: Class<Doc>[] = []
if (ofClass !== undefined) {
// We need to include all possible mixins as well
for (const ancestor of hierarchy.getAncestors(ofClass)) {
if (ancestor === ofClass) {
continue
}
const mixins = hierarchy.getDescendants(ancestor).filter((it) => hierarchy.isMixin(it))
for (const m of mixins) {
const mm = hierarchy.getClass(m)
if (!classes.includes(m) && mm.extends === ancestor && mm.label !== undefined) {
// Check if parent of
classes.push(m)
}
clQuery.query(core.class.Class, {}, (res) => {
rawClasses = res
})
$: classes = filterDescendants(hierarchy, ofClass, rawClasses)
$: if (ofClass !== undefined) {
// We need to include all possible mixins as well
for (const ancestor of hierarchy.getAncestors(ofClass)) {
if (ancestor === ofClass) {
continue
}
const mixins = hierarchy.getDescendants(ancestor).filter((it) => hierarchy.isMixin(it))
for (const m of mixins) {
const mm = hierarchy.getClass(m)
if (
!classes.includes(m) &&
mm.extends === ancestor &&
mm.label !== undefined &&
client.getHierarchy().hasMixin(mm, setting.mixin.Editable)
) {
// Check if parent of
classes.push(m)
}
}
}
})
}
$: if (ofClass !== undefined && _class !== undefined && !client.getHierarchy().isDerived(_class, ofClass)) {
_class = ofClass
}
</script>
<div class="antiComponent">
@ -91,7 +104,11 @@
</div>
<div class="ac-column max">
{#if _class !== undefined}
<ClassAttributes {_class} {ofClass} {attributeMapper} />
<table class="antiTable">
<tbody>
<ClassAttributes {_class} {ofClass} {attributeMapper} {useOfClassAttributes} />
</tbody>
</table>
{/if}
</div>
</div>

View File

@ -95,6 +95,8 @@
index = e.detail?.index
defaultValue = e.detail?.defaultValue
}
$: clazz = client.getHierarchy().getClass(_class)
</script>
<Card
@ -106,6 +108,13 @@
}}
on:changeContent
>
<svelte:fragment slot="title">
<div class="flex-row-center">
<Label label={setting.string.CreatingAttribute} />
<div class="p-1">></div>
<Label label={clazz.label} />
</div>
</svelte:fragment>
<div class="mb-2"><EditBox bind:value={name} placeholder={core.string.Name} /></div>
<div class="flex-col mb-2">
<div class="flex-row-center flex-grow">

View File

@ -35,7 +35,7 @@
}
})
async function setInviteSettings () {
async function setInviteSettings (): Promise<void> {
const newSettings = {
expirationTime: expTime,
emailMask: mask,

View File

@ -81,7 +81,7 @@
{items}
selected={account.role?.toString()}
on:selected={(e) => {
change(account, Number(e.detail))
void change(account, Number(e.detail))
}}
/>
</div>

View File

@ -100,7 +100,7 @@
{disabled}
kind={'primary'}
on:click={() => {
save()
void save()
}}
/>
</div>

View File

@ -22,6 +22,7 @@
import { Button, createFocusManager, EditBox, FocusHandler, Icon, Label, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import setting from '../plugin'
const client = getClient()
let avatarEditor: EditableAvatar
@ -34,17 +35,17 @@
onDestroy(
employeeByIdStore.subscribe((p) => {
const emp = p.get(account.person as Ref<Employee>)
if (emp) {
if (emp !== undefined) {
firstName = getFirstName(emp.name)
lastName = getLastName(emp.name)
}
})
)
async function onAvatarDone (e: any) {
async function onAvatarDone (e: any): Promise<void> {
if (employee === undefined) return
if (employee.avatar) {
if (employee.avatar != null) {
await avatarEditor.removeAvatar(employee.avatar)
}
const avatar = await avatarEditor.createAvatar()
@ -72,9 +73,9 @@
)
}
async function nameChange () {
if (employee) {
await client.update(employee, {
async function nameChange (): Promise<void> {
if (employee !== undefined) {
await client.diffUpdate(employee, {
name: combineName(firstName, lastName)
})
}
@ -142,7 +143,7 @@
icon={setting.icon.Signout}
label={setting.string.Leave}
on:click={() => {
leave()
void leave()
}}
/>
</div>

View File

@ -71,8 +71,12 @@
}
function selectCategory (id: string): void {
const loc = getCurrentResolvedLocation()
loc.path[3] = id
loc.path.length = 4
if (loc.path[3] === id) {
loc.path.length = 3
} else {
loc.path[3] = id
loc.path.length = 4
}
navigate(loc)
}
function signOut (): void {
@ -86,7 +90,7 @@
setMetadata(presentation.metadata.Token, null)
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
closeClient()
void closeClient()
navigate({ path: [loginId] })
}
function selectWorkspace (): void {
@ -105,20 +109,43 @@
<div class="antiPanel-wrap__content">
<NavHeader label={setting.string.Settings} />
<Scroller shrink>
{#each categories as category, i}
{#if i > 0 && categories[i - 1].group !== category.group}
<Scroller>
{#each categories as _category, i}
{#if i > 0 && categories[i - 1].group !== _category.group}
<div class="antiNav-divider short line" />
{/if}
<CategoryElement
icon={category.icon}
label={category.label}
selected={category.name === categoryId}
expandable={category._id === setting.ids.Setting}
icon={_category.icon}
label={_category.label}
selected={_category.name === categoryId}
expandable={_category.expandable ?? _category._id === setting.ids.Setting}
on:click={() => {
selectCategory(category.name)
selectCategory(_category.name)
}}
/>
>
<svelte:fragment slot="tools">
{#if _category.extraComponents?.tools}
<Component
is={_category.extraComponents?.tools}
props={{
visibleNav,
kind: 'tools',
categoryName: _category.name
}}
/>
{/if}
</svelte:fragment>
</CategoryElement>
{#if _category.extraComponents?.navigation}
<Component
is={_category.extraComponents?.navigation}
props={{
visibleNav,
kind: 'navigation',
categoryName: _category.name
}}
/>
{/if}
{/each}
<div class="antiNav-space" />
</Scroller>

View File

@ -17,21 +17,12 @@
import { AccountRole, getCurrentAccount } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import setting, { SettingsCategory } from '@hcengineering/setting'
import {
Component,
Label,
Location,
Scroller,
Separator,
defineSeparators,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore,
settingsSeparators
} from '@hcengineering/ui'
import { Component, Location, getCurrentResolvedLocation, navigate, resolvedLocationStore } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import CategoryElement from './CategoryElement.svelte'
export let kind: 'navigation' | undefined
export let categoryName: string
let category: SettingsCategory | undefined
let categoryId: string = ''
@ -66,42 +57,38 @@
function selectCategory (id: string): void {
const loc = getCurrentResolvedLocation()
loc.path[4] = id
loc.path.length = 5
loc.path[3] = categoryName
if (loc.path[4] === id) {
loc.path.length = 4
} else {
loc.path[4] = id
loc.path.length = 5
}
navigate(loc)
}
defineSeparators('settingWorkspace', settingsSeparators)
</script>
<div class="flex h-full clear-mins">
{#if visibleNav}
<div class="antiPanel-navigator filledNav indent">
<div class="antiPanel-wrap__content">
<div class="antiNav-header overflow-label">
<Label label={setting.string.WorkspaceSetting} />
</div>
<Scroller shrink>
{#each categories as category}
<CategoryElement
icon={category.icon}
label={category.label}
selected={category.name === categoryId}
on:click={() => {
selectCategory(category.name)
}}
/>
{/each}
<div class="antiNav-space" />
</Scroller>
</div>
{#if kind === 'navigation'}
<div class="ml-4 mt-2">
<div class="antiPanel-element">
{#each categories as category}
<CategoryElement
icon={category.icon}
label={category.label}
selected={category.name === categoryId}
on:click={() => {
selectCategory(category.name)
}}
/>
{#if category.name === categoryId && category.extraComponents?.navigation}
<Component
is={category.extraComponents?.navigation}
props={{ kind: 'navigation', categoryName: categoryId }}
/>
{/if}
{/each}
</div>
<Separator name={'settingWorkspace'} index={0} color={'var(--theme-navpanel-border)'} />
{/if}
<div class="antiPanel-component filled">
{#if category}
<Component is={category.component} />
{/if}
</div>
</div>
{:else if category}
<Component is={category.component} props={{ kind: 'content' }} />
{/if}

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