UBER-430: Remove old migrations (#3398)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-06-12 17:40:04 +07:00 committed by GitHub
parent ed964ed362
commit e809a67451
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 53 additions and 3625 deletions

View File

@ -13,10 +13,8 @@
// limitations under the License.
//
import { Comment, Message, ThreadMessage } from '@hcengineering/chunter'
import core, { DOMAIN_TX, Doc, Ref, TxCreateDoc, TxOperations } from '@hcengineering/core'
import core, { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { DOMAIN_CHUNTER, DOMAIN_COMMENT } from './index'
import chunter from './plugin'
export async function createGeneral (tx: TxOperations): Promise<void> {
@ -81,93 +79,8 @@ async function createBacklink (tx: TxOperations): Promise<void> {
}
}
export async function migrateMessages (client: MigrationClient): Promise<void> {
const messages = await client.find(DOMAIN_CHUNTER, {
_class: chunter.class.Message,
attachedTo: { $exists: false }
})
for (const message of messages) {
await client.update(
DOMAIN_CHUNTER,
{
_id: message._id
},
{
attachedTo: message.space,
attachedToClass: chunter.class.Channel,
collection: 'messages'
}
)
}
const txes = await client.find<TxCreateDoc<Doc>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectClass: chunter.class.Message
})
for (const tx of txes) {
await client.update(
DOMAIN_TX,
{
_id: tx._id
},
{
'attributes.attachedTo': tx.objectSpace,
'attributes.attachedToClass': chunter.class.Channel,
'attributes.collection': 'messages'
}
)
}
}
export async function migrateThreadMessages (client: MigrationClient): Promise<void> {
const messages = await client.find<Comment>(DOMAIN_COMMENT, {
_class: chunter.class.Comment,
attachedToClass: chunter.class.Message
})
for (const message of messages) {
await client.delete(DOMAIN_COMMENT, message._id)
await client.create<ThreadMessage>(DOMAIN_CHUNTER, {
attachedTo: message.attachedTo as Ref<Message>,
attachedToClass: message.attachedToClass,
attachments: message.attachments,
content: message.message,
collection: message.collection,
_class: chunter.class.ThreadMessage,
space: message.space,
modifiedOn: message.modifiedOn,
modifiedBy: message.modifiedBy,
createBy: message.modifiedBy,
createdOn: message.modifiedOn,
_id: message._id as string as Ref<ThreadMessage>
})
}
const txes = await client.find<TxCreateDoc<Comment>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectClass: chunter.class.Comment,
'attributes.attachedToClass': chunter.class.Message
})
for (const tx of txes) {
await client.update(
DOMAIN_TX,
{
_id: tx._id
},
{
objectClass: chunter.class.ThreadMessage,
'attributes.createBy': tx.modifiedBy,
'attributes.createdOn': tx.modifiedOn,
'attributes.content': tx.attributes.message
}
)
}
}
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await migrateMessages(client)
await migrateThreadMessages(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createGeneral(tx)

View File

@ -13,149 +13,9 @@
// limitations under the License.
//
import core, {
AttachedDoc,
DOMAIN_BLOB,
DOMAIN_DOC_INDEX_STATE,
DOMAIN_MODEL,
DOMAIN_TX,
Doc,
TxCollectionCUD,
TxCreateDoc
} from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
async function fillCreatedBy (client: MigrationClient): Promise<void> {
const h = client.hierarchy
const domains = h.domains()
for (const domain of domains) {
if (
domain === DOMAIN_TX ||
domain === DOMAIN_MODEL ||
domain === DOMAIN_BLOB ||
domain === DOMAIN_DOC_INDEX_STATE
) {
continue
}
while (true) {
try {
const objects = await client.find<Doc>(
domain,
{ createdBy: { $exists: false } },
{ projection: { _id: 1, modifiedBy: 1 }, limit: 10000 }
)
if (objects.length === 0) {
break
}
const txes = await client.find<TxCreateDoc<Doc>>(
DOMAIN_TX,
{
_class: core.class.TxCreateDoc,
objectId: { $in: Array.from(objects.map((it) => it._id)) }
},
{ projection: { _id: 1, modifiedBy: 1, createdBy: 1, objectId: 1 } }
)
const txes2 = (
await client.find<TxCollectionCUD<Doc, AttachedDoc>>(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc,
'tx.objectId': { $in: Array.from(objects.map((it) => it._id)) }
},
{ projection: { _id: 1, modifiedBy: 1, createdBy: 1, tx: 1 } }
)
).map((it) => it.tx as unknown as TxCreateDoc<Doc>)
const txMap = new Map(txes.concat(txes2).map((p) => [p.objectId, p]))
console.log('migrateCreateBy', domain, objects.length)
await client.bulk(
domain,
objects.map((it) => {
const createTx = txMap.get(it._id)
return {
filter: { _id: it._id },
update: {
createdBy: createTx?.modifiedBy ?? it.modifiedBy
}
}
})
)
} catch (err) {}
}
}
}
async function fillCreatedOn (client: MigrationClient): Promise<void> {
const h = client.hierarchy
const domains = h.domains()
for (const domain of domains) {
if (
domain === DOMAIN_TX ||
domain === DOMAIN_MODEL ||
domain === DOMAIN_BLOB ||
domain === DOMAIN_DOC_INDEX_STATE
) {
continue
}
await client.update<Doc>(domain, { createOn: { $exists: true } }, { $unset: { createOn: 1 } })
while (true) {
try {
const objects = await client.find<Doc>(
domain,
{ createdOn: { $exists: false } },
{ projection: { _id: 1, modifiedOn: 1 }, limit: 10000 }
)
if (objects.length === 0) {
break
}
const txes = await client.find<TxCreateDoc<Doc>>(
DOMAIN_TX,
{
_class: core.class.TxCreateDoc,
objectId: { $in: Array.from(objects.map((it) => it._id)) }
},
{ projection: { _id: 1, modifiedOn: 1, createdOn: 1, objectId: 1 } }
)
const txes2 = (
await client.find<TxCollectionCUD<Doc, AttachedDoc>>(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc,
'tx.objectId': { $in: Array.from(objects.map((it) => it._id)) }
},
{ projection: { _id: 1, modifiedOn: 1, createdOn: 1, tx: 1 } }
)
).map((it) => it.tx as unknown as TxCreateDoc<Doc>)
const txMap = new Map(txes.concat(txes2).map((p) => [p.objectId, p]))
console.log('migratecreatedOn', domain, objects.length)
await client.bulk(
domain,
objects.map((it) => {
const createTx = txMap.get(it._id)
return {
filter: { _id: it._id },
update: {
createdOn: createTx?.createdOn ?? it.modifiedOn
}
}
})
)
} catch (err) {}
}
}
}
export const coreOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await fillCreatedBy(client)
await fillCreatedOn(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -13,12 +13,10 @@
// limitations under the License.
//
import contact, { Employee } from '@hcengineering/contact'
import { DOMAIN_TX, Ref, toIdMap, TxCollectionCUD, TxCreateDoc, TxOperations, TxUpdateDoc } from '@hcengineering/core'
import { Department, Request, TzDate } from '@hcengineering/hr'
import { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import hr, { DOMAIN_HR } from './index'
import core from '@hcengineering/model-core'
import hr from './index'
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
@ -42,216 +40,10 @@ async function createSpace (tx: TxOperations): Promise<void> {
}
}
async function fixDuplicatesInDepartments (tx: TxOperations): Promise<void> {
const departments = await tx.findAll(hr.class.Department, {})
const departmentUpdate = departments.map((department) => {
const uniqueMembers = [...new Set(department.members)]
return tx.update(department, { members: uniqueMembers })
})
await Promise.all(departmentUpdate)
}
async function fixDepartmentsFromStaff (tx: TxOperations): Promise<void> {
const departments = await tx.findAll(hr.class.Department, {})
const parentsWithDepartmentMap: Map<Ref<Department>, Department[]> = new Map()
const departmentsMap = toIdMap(departments)
const ancestors: Map<Ref<Department>, Ref<Department>> = new Map()
for (const department of departments) {
if (department._id === hr.ids.Head) continue
ancestors.set(department._id, department.space)
}
for (const departmentItem of departments) {
const parents: Department[] = parentsWithDepartmentMap.get(departmentItem._id) ?? []
let _id = departmentItem._id
while (true) {
const department = departmentsMap.get(_id)
if (department === undefined) break
if (!parents.includes(department)) parents.push(department)
const next = ancestors.get(department._id)
if (next === undefined) break
_id = next
}
parentsWithDepartmentMap.set(departmentItem._id, parents)
}
const staff = await tx.findAll(hr.mixin.Staff, {})
const promises = []
const employeeAccountByEmployeeMap = new Map(
(await tx.findAll(contact.class.EmployeeAccount, {})).map((ea) => [ea.employee, ea])
)
for (const st of staff) {
if (st.department == null) continue
const correctDepartments: Department[] = parentsWithDepartmentMap.get(st.department) ?? []
promises.push(
...departments
.filter((department) => !correctDepartments.includes(department))
.map((dep) => {
const employeeAccount = employeeAccountByEmployeeMap.get(st._id)
if (employeeAccount == null) return []
return tx.update(dep, { $pull: { members: employeeAccount._id } })
})
)
}
await Promise.all(promises)
}
async function fixInvalidRequests (tx: TxOperations): Promise<void> {
const departments = await tx.findAll(hr.class.Department, {})
const staff = await tx.findAll(hr.mixin.Staff, {})
const staffDepartmentMap = new Map(staff.map((s) => [s._id, s.department]))
const requests = await tx.findAll(hr.class.Request, { space: { $nin: departments.map((d) => d._id) } })
const res = []
for (const request of requests) {
const currentStaffDepartment = staffDepartmentMap.get(request.attachedTo)
if (currentStaffDepartment !== null) {
res.push(tx.update(request, { space: currentStaffDepartment }))
} else {
res.push(tx.update(request, { space: hr.ids.Head }))
}
}
await Promise.all(res)
}
function toTzDate (date: number): TzDate {
const res = new Date(date)
return {
year: res.getFullYear(),
month: res.getMonth(),
day: res.getDate(),
offset: res.getTimezoneOffset()
}
}
async function migrateRequestTime (client: MigrationClient, request: Request): Promise<void> {
const date = toTzDate((request as any).date as unknown as number)
const dueDate = toTzDate((request as any).dueDate as unknown as number)
await client.update(
DOMAIN_HR,
{ _id: request._id },
{
tzDate: date,
tzDueDate: dueDate
}
)
const txes = await client.find<TxCollectionCUD<Employee, Request>>(DOMAIN_TX, {
'tx._class': { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc] },
'tx.objectId': request._id
})
for (const utx of txes) {
if (utx.tx._class === core.class.TxCreateDoc) {
const ctx = utx.tx as TxCreateDoc<Request>
const { date, dueDate, ...attributes } = ctx.attributes as any
await client.update(
DOMAIN_TX,
{ _id: utx._id },
{
tx: {
...ctx,
attributes: {
...attributes,
tzDate: toTzDate(date as unknown as number),
tzDueDate: toTzDate((dueDate ?? date) as unknown as number)
}
}
}
)
}
if (utx.tx._class === core.class.TxUpdateDoc) {
const ctx = utx.tx as TxUpdateDoc<Request>
const { date, dueDate, ...operations } = ctx.operations as any
const ops: any = {
...operations
}
if (date !== undefined) {
ops.tzDate = toTzDate(date as unknown as number)
}
if (dueDate !== undefined) {
ops.tzDueDate = toTzDate(dueDate as unknown as number)
}
await client.update(
DOMAIN_TX,
{ _id: utx._id },
{
tx: {
...ctx,
operations: ops
}
}
)
}
}
}
async function migrateTime (client: MigrationClient): Promise<void> {
const createTxes = await client.find<TxCreateDoc<Request>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectClass: hr.class.Request
})
for (const tx of createTxes) {
await client.update(
DOMAIN_TX,
{ _id: tx._id },
{
_class: core.class.TxCollectionCUD,
tx,
collection: tx.attributes.collection,
objectId: tx.attributes.attachedTo,
objectClass: tx.attributes.attachedToClass
}
)
await client.update(
DOMAIN_TX,
{ _id: tx._id },
{
$unset: {
attributes: ''
}
}
)
}
const requests = await client.find<Request>(DOMAIN_HR, { _class: hr.class.Request, tzDate: { $exists: false } })
for (const request of requests) {
await migrateRequestTime(client, request)
}
}
async function fillManagers (client: MigrationClient): Promise<void> {
await client.update<Department>(
DOMAIN_SPACE,
{
_class: hr.class.Department,
managers: { $exists: false }
},
{
managers: []
}
)
await client.update<TxCreateDoc<Department>>(
DOMAIN_TX,
{
_class: core.class.TxCreateDoc,
objectClass: hr.class.Department,
'attributes.managers': { $exists: false }
},
{
'attributes.managers': []
}
)
}
export const hrOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await migrateTime(client)
await fillManagers(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createSpace(tx)
await fixDuplicatesInDepartments(tx)
await fixDepartmentsFromStaff(tx)
await fixInvalidRequests(tx)
}
}

View File

@ -13,121 +13,9 @@
// limitations under the License.
//
import core, {
Account,
AttachedDoc,
Class,
Collection,
Data,
Doc,
DOMAIN_TX,
Ref,
Timestamp,
toIdMap,
Tx,
TxCUD,
TxOperations
} from '@hcengineering/core'
import core, { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import notification, { DocUpdates, DocUpdateTx, NotificationType } from '@hcengineering/notification'
import { DOMAIN_NOTIFICATION } from '.'
async function fillNotificationText (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_NOTIFICATION,
{ _class: notification.class.Notification, text: { $exists: false } },
{
text: ''
}
)
await client.update(
DOMAIN_TX,
{
_class: core.class.TxCreateDoc,
objectClass: notification.class.Notification,
'attributes.text': { $exists: false }
},
{
'attributes.text': ''
}
)
}
interface OldDocUpdates extends Doc {
user: Ref<Account>
attachedTo: Ref<Doc>
attachedToClass: Ref<Class<Doc>>
hidden: boolean
lastTx?: Ref<TxCUD<Doc>>
lastTxTime?: Timestamp
txes: [Ref<TxCUD<Doc>>, Timestamp][]
}
async function fillDocUpdates (client: MigrationClient): Promise<void> {
const notifications = await client.find<OldDocUpdates>(DOMAIN_NOTIFICATION, {
_class: notification.class.DocUpdates,
lastTx: { $exists: true }
})
while (notifications.length > 0) {
const docs = notifications.splice(0, 1000)
const txIds = docs
.map((p) => {
const res = p.txes.map((p) => p[0])
if (p.lastTx !== undefined) {
res.push(p.lastTx)
}
return res
})
.flat()
const txes = await client.find<Tx>(DOMAIN_TX, { _id: { $in: txIds } })
const txesMap = toIdMap(txes)
for (const doc of docs) {
const txes: DocUpdateTx[] = doc.txes
.map((p) => {
const tx = txesMap.get(p[0])
if (tx === undefined) return undefined
const res: DocUpdateTx = {
_id: tx._id as Ref<TxCUD<Doc>>,
modifiedBy: tx.modifiedBy,
modifiedOn: tx.modifiedOn,
isNew: true
}
return res
})
.filter((p) => p !== undefined) as DocUpdateTx[]
if (txes.length === 0 && doc.lastTx !== undefined) {
const tx = txesMap.get(doc.lastTx)
if (tx !== undefined) {
txes.unshift({
_id: tx._id as Ref<TxCUD<Doc>>,
modifiedBy: tx.modifiedBy,
modifiedOn: tx.modifiedOn,
isNew: false
})
}
}
await client.update(
DOMAIN_NOTIFICATION,
{
_id: doc._id
},
{
$unset: { lastTx: 1 },
$set: {
txes
}
}
)
}
}
}
async function removeSettings (client: MigrationClient): Promise<void> {
const outdatedSettings = await client.find(DOMAIN_NOTIFICATION, { _class: notification.class.NotificationSetting })
for (const setting of outdatedSettings) {
await client.delete(DOMAIN_NOTIFICATION, setting._id)
}
}
import notification from '@hcengineering/notification'
async function createSpace (client: MigrationUpgradeClient): Promise<void> {
const txop = new TxOperations(client, core.account.System)
@ -150,123 +38,9 @@ async function createSpace (client: MigrationUpgradeClient): Promise<void> {
}
}
async function fillCollaborators (client: MigrationClient): Promise<void> {
const targetClasses = await client.model.findAll(notification.mixin.ClassCollaborators, {})
for (const targetClass of targetClasses) {
const domain = client.hierarchy.getDomain(targetClass._id)
const desc = client.hierarchy.getDescendants(targetClass._id)
await client.update(
domain,
{
_class: { $in: desc },
'notification:mixin:Collaborators': { $exists: false }
},
{
'notification:mixin:Collaborators': {
collaborators: []
}
}
)
}
}
async function fillDocUpdatesHidder (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_NOTIFICATION,
{
_class: notification.class.DocUpdates,
hidden: { $exists: false }
},
{
hidden: false
}
)
}
async function createCustomFieldTypes (client: MigrationUpgradeClient): Promise<void> {
const txop = new TxOperations(client, core.account.System)
const attributes = await client.findAll(core.class.Attribute, { isCustom: true })
const groups = new Map(
(await client.findAll(notification.class.NotificationGroup, {}))
.filter((p) => p.objectClass !== undefined)
.map((p) => [p.objectClass, p])
)
const types = new Set((await client.findAll(notification.class.NotificationType, {})).map((p) => p.attribute))
for (const attribute of attributes) {
if (attribute.hidden === true || attribute.readonly === true) continue
if (types.has(attribute._id)) continue
const group = groups.get(attribute.attributeOf)
if (group === undefined) continue
const isCollection: boolean = core.class.Collection === attribute.type._class
const _class = attribute.attributeOf
const objectClass = !isCollection ? _class : (attribute.type as Collection<AttachedDoc>).of
const txClasses = !isCollection
? [client.getHierarchy().isMixin(_class) ? core.class.TxMixin : core.class.TxUpdateDoc]
: [core.class.TxCreateDoc, core.class.TxRemoveDoc]
const data: Data<NotificationType> = {
attribute: attribute._id,
field: attribute.name,
group: group._id,
generated: true,
objectClass,
txClasses,
hidden: false,
providers: {
[notification.providers.PlatformNotification]: false
},
label: attribute.label
}
if (isCollection) {
data.attachedToClass = _class
}
const id = `${notification.class.NotificationType}_${_class}_${attribute.name}` as Ref<NotificationType>
await txop.createDoc(notification.class.NotificationType, core.space.Model, data, id)
}
}
async function changeDocUpdatesSpaces (client: MigrationUpgradeClient): Promise<void> {
const txop = new TxOperations(client, core.account.System)
const docUpdates = await client.findAll(notification.class.DocUpdates, { space: notification.space.Notifications })
const map = new Map<Ref<Class<Doc>>, Map<Ref<Doc>, DocUpdates[]>>()
for (const docUpdate of docUpdates) {
const _class = map.get(docUpdate.attachedToClass) ?? new Map()
const arr = _class.get(docUpdate.attachedTo) ?? []
arr.push(docUpdate)
_class.set(docUpdate.attachedTo, arr)
map.set(docUpdate.attachedToClass, _class)
}
for (const [_class, arr] of map) {
const ids = Array.from(arr.keys())
const docs = await client.findAll(_class, { _id: { $in: ids } })
for (const doc of docs) {
const updateDocs = arr.get(doc._id)
if (updateDocs === undefined) continue
await Promise.all(updateDocs.map(async (p) => await txop.update(p, { space: doc.space })))
}
}
}
async function cleanOutdatedSettings (client: MigrationClient): Promise<void> {
const res = await client.find(DOMAIN_NOTIFICATION, {
_class: notification.class.NotificationSetting
})
for (const value of res) {
await client.delete(DOMAIN_NOTIFICATION, value._id)
}
}
export const notificationOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await removeSettings(client)
await fillNotificationText(client)
await fillCollaborators(client)
await fillDocUpdatesHidder(client)
await fillDocUpdates(client)
await cleanOutdatedSettings(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
await createSpace(client)
await createCustomFieldTypes(client)
await changeDocUpdatesSpaces(client)
}
}

View File

@ -14,133 +14,16 @@
//
import { getCategories } from '@anticrm/skillset'
import { Organization } from '@hcengineering/contact'
import core, {
Doc,
DOMAIN_TX,
Ref,
Space,
TxCreateDoc,
TxFactory,
TxOperations,
TxProcessor
} from '@hcengineering/core'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { DOMAIN_CALENDAR } from '@hcengineering/model-calendar'
import contact, { DOMAIN_CONTACT } from '@hcengineering/model-contact'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import core, { Doc, Ref, Space, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import tags, { TagCategory } from '@hcengineering/model-tags'
import { createKanbanTemplate, createSequence, DOMAIN_KANBAN } from '@hcengineering/model-task'
import { Vacancy } from '@hcengineering/recruit'
import task, { KanbanTemplate, Sequence } from '@hcengineering/task'
import recruit from './plugin'
import { createKanbanTemplate, createSequence } from '@hcengineering/model-task'
import task, { KanbanTemplate } from '@hcengineering/task'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
async function fixImportedTitle (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_CONTACT,
{
title: { $exists: true }
},
{
$rename: { title: 'recruit:mixin:Candidate.title' }
}
)
}
async function fillVacancyNumbers (client: MigrationClient): Promise<void> {
const docs = await client.find<Vacancy>(DOMAIN_SPACE, {
_class: recruit.class.Vacancy,
number: { $exists: false }
})
if (docs.length === 0) return
const txex = await client.find<TxCreateDoc<Vacancy>>(DOMAIN_TX, {
objectId: { $in: docs.map((it) => it._id) },
_class: core.class.TxCreateDoc
})
let number = 1
for (const doc of docs) {
await client.update(
DOMAIN_SPACE,
{
_id: doc._id
},
{
number
}
)
const tx = txex.find((it) => it.objectId === doc._id)
if (tx !== undefined) {
await client.update(
DOMAIN_TX,
{
_id: tx._id
},
{
'attributes.number': number
}
)
}
number++
}
const current = await client.find<Sequence>(DOMAIN_KANBAN, {
_class: task.class.Sequence,
attachedto: recruit.class.Vacancy
})
if (current.length === 0) {
const factory = new TxFactory(core.account.System)
const tx = factory.createTxCreateDoc(task.class.Sequence, task.space.Sequence, {
attachedTo: recruit.class.Vacancy,
sequence: number
})
const doc = TxProcessor.createDoc2Doc(tx)
await client.create(DOMAIN_KANBAN, doc)
await client.create(DOMAIN_TX, tx)
}
}
import recruit from './plugin'
export const recruitOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await fixImportedTitle(client)
await fillVacancyNumbers(client)
await client.update(
DOMAIN_CALENDAR,
{
_class: recruit.class.Review,
space: { $nin: [recruit.space.Reviews] }
},
{
space: recruit.space.Reviews
}
)
const vacancies = await client.find<Vacancy>(
DOMAIN_SPACE,
{ _class: recruit.class.Vacancy, company: { $exists: true } },
{ projection: { _id: 1, company: 1 } }
)
const orgIds = Array.from(vacancies.map((it) => it.company))
.filter((it) => it != null)
.filter((it, idx, arr) => arr.indexOf(it) === idx) as Ref<Organization>[]
const orgs = await client.find<Organization>(DOMAIN_CONTACT, {
_class: contact.class.Organization,
_id: { $in: orgIds }
})
for (const o of orgs) {
if ((o as any)[recruit.mixin.VacancyList] === undefined) {
await client.update(
DOMAIN_CONTACT,
{ _id: o._id },
{
[recruit.mixin.VacancyList]: {
vacancies: vacancies.filter((it) => it.company === o._id).reduce((a) => a + 1, 0)
}
}
)
}
}
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)

View File

@ -13,39 +13,9 @@
// limitations under the License.
//
import core, { DOMAIN_TX, Doc, TxOperations } from '@hcengineering/core'
import core, { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import setting from './plugin'
import { DOMAIN_SETTING } from '.'
import notification, { Collaborators } from '@hcengineering/notification'
async function migrateIntegrationsSpace (client: MigrationClient): Promise<void> {
const settings = await client.find(DOMAIN_SETTING, {
_class: setting.class.Integration,
space: { $ne: setting.space.Setting }
})
for (const object of settings) {
await client.update(
DOMAIN_SETTING,
{
_id: object._id
},
{
space: setting.space.Setting
}
)
await client.update(
DOMAIN_TX,
{
objectId: object._id,
objectSpace: { $ne: setting.space.Setting }
},
{
objectSpace: setting.space.Setting
}
)
}
}
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
@ -67,49 +37,10 @@ async function createSpace (tx: TxOperations): Promise<void> {
}
}
async function fillMigrationCollaborator (tx: TxOperations): Promise<void> {
const h = tx.getHierarchy()
const settings = await tx.findAll(setting.class.Integration, {})
for (const value of settings) {
if (h.hasMixin(value, notification.mixin.Collaborators)) {
const collabs = h.as<Doc, Collaborators>(value, notification.mixin.Collaborators)
const target = collabs.createdBy ?? collabs.modifiedBy
if (collabs.collaborators === undefined || !collabs.collaborators.includes(target)) {
const res = tx.txFactory.createTxMixin<Doc, Collaborators>(
value._id,
value._class,
value.space,
notification.mixin.Collaborators,
{
collaborators: [target]
}
)
res.space = core.space.DerivedTx
await tx.tx(res)
}
} else {
const res = tx.txFactory.createTxMixin<Doc, Collaborators>(
value._id,
setting.class.Integration,
value.space,
notification.mixin.Collaborators,
{
collaborators: [value.createdBy ?? value.modifiedBy]
}
)
res.space = core.space.DerivedTx
await tx.tx(res)
}
}
}
export const settingOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await migrateIntegrationsSpace(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createSpace(tx)
await fillMigrationCollaborator(tx)
}
}

View File

@ -1,51 +1,9 @@
import core, { Ref, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@hcengineering/model'
import { TagElement, TagReference } from '@hcengineering/tags'
import { DOMAIN_TAGS } from './index'
import core, { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import tags from './plugin'
async function updateTagRefCount (client: MigrationClient): Promise<void> {
const tagElements = await client.find(DOMAIN_TAGS, { _class: tags.class.TagElement, refCount: { $exists: false } })
const refs = await client.find<TagReference>(DOMAIN_TAGS, {
_class: tags.class.TagReference,
tag: { $in: tagElements.map((p) => p._id as Ref<TagElement>) }
})
const map = new Map<Ref<TagElement>, number>()
for (const ref of refs) {
map.set(ref.tag, (map.get(ref.tag) ?? 0) + 1)
}
const promises: Promise<MigrationResult>[] = []
for (const tag of map) {
promises.push(
client.update(
DOMAIN_TAGS,
{
_id: tag[0]
},
{
refCount: tag[1]
}
)
)
}
await Promise.all(promises)
}
export const tagsOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_TAGS,
{
_class: tags.class.TagElement,
category: 'tags:category:Other'
},
{
category: 'recruit:category:Other'
}
)
await updateTagRefCount(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
const current = await tx.findOne(core.class.Space, {

View File

@ -13,12 +13,11 @@
// limitations under the License.
//
import { Class, Doc, Domain, Ref, Space, TxOperations, DOMAIN_STATUS } from '@hcengineering/core'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import { Class, Doc, Domain, Ref, Space, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import tags from '@hcengineering/model-tags'
import { DoneStateTemplate, genRanks, KanbanTemplate, StateTemplate } from '@hcengineering/task'
import { DOMAIN_TASK, DOMAIN_KANBAN } from '.'
import { DoneStateTemplate, KanbanTemplate, StateTemplate, genRanks } from '@hcengineering/task'
import task from './plugin'
/**
@ -131,57 +130,8 @@ async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultSequence(tx)
}
async function migrateTodoItems (client: MigrationClient): Promise<void> {
const assigneeTodos = await client.find(DOMAIN_TASK, { _class: task.class.TodoItem, assignee: { $exists: false } })
for (const todo of assigneeTodos) {
await client.update(DOMAIN_TASK, { _id: todo._id }, { assignee: null })
}
const dueToTodos = await client.find(DOMAIN_TASK, { _class: task.class.TodoItem, dueTo: { $exists: false } })
for (const todo of dueToTodos) {
await client.update(DOMAIN_TASK, { _id: todo._id }, { dueTo: null })
}
}
export const taskOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await Promise.all([migrateTodoItems(client)])
const stateClasses = client.hierarchy.getDescendants(task.class.State)
const doneStateClasses = client.hierarchy.getDescendants(task.class.DoneState)
const stateTemplateClasses = client.hierarchy.getDescendants(task.class.StateTemplate)
const doneStateTemplatesClasses = client.hierarchy.getDescendants(task.class.DoneStateTemplate)
try {
await client.move(DOMAIN_STATE, { _class: { $in: [...stateClasses, ...doneStateClasses] } }, DOMAIN_STATUS)
} catch (err) {}
await client.update(
DOMAIN_STATUS,
{ _class: { $in: stateClasses }, ofAttribute: { $exists: false } },
{ ofAttribute: task.attribute.State }
)
await client.update(
DOMAIN_STATUS,
{ _class: { $in: doneStateClasses }, ofAttribute: { $exists: false } },
{ ofAttribute: task.attribute.DoneState }
)
await client.update(
DOMAIN_STATUS,
{ _class: { $in: [...stateClasses, ...doneStateClasses] }, title: { $exists: true } },
{ $rename: { title: 'name' } }
)
await client.update(
DOMAIN_KANBAN,
{ _class: { $in: [...stateTemplateClasses, ...doneStateTemplatesClasses] }, title: { $exists: true } },
{ $rename: { title: 'name' } }
)
await client.delete(DOMAIN_SPACE, 'task:space:ProjectTemplates' as Space['_id'])
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)

View File

@ -13,15 +13,12 @@
// limitations under the License.
//
import core, { DOMAIN_TX, TxOperations } from '@hcengineering/core'
import core, { TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import templates from './plugin'
export const templatesOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await changeClass(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
const current = await tx.findOne(core.class.Space, {
@ -45,34 +42,3 @@ export const templatesOperation: MigrateOperation = {
}
}
}
async function changeClass (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_SPACE,
{
_id: templates.space.Templates,
_class: core.class.Space
},
{
_class: templates.class.TemplateCategory,
private: false,
name: 'Public templates',
description: 'Space for public templates'
}
)
await client.update(
DOMAIN_TX,
{
objectId: templates.space.Templates,
objectClass: core.class.Space,
_class: core.class.TxCreateDoc
},
{
objectClass: templates.class.TemplateCategory,
'attributes.private': false,
'attributes.name': 'Public templates',
'attributes.description': 'Space for public templates'
}
)
}

View File

@ -14,7 +14,7 @@
//
import activity from '@hcengineering/activity'
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
import contact, { Employee } from '@hcengineering/contact'
import {
DOMAIN_MODEL,
DateRangeMode,
@ -42,7 +42,6 @@ import {
TypeNumber,
TypeRef,
TypeString,
TypeTimestamp,
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
@ -67,8 +66,6 @@ import {
Milestone,
MilestoneStatus,
Project,
Scrum,
ScrumRecord,
TimeReportDayType,
TimeSpendReport,
trackerId
@ -134,9 +131,6 @@ export class TProject extends TSpace implements Project {
@Hidden()
sequence!: number
@Prop(Collection(tracker.class.IssueStatus), tracker.string.IssueStatuses)
issueStatuses!: number
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus)
defaultIssueStatus!: Ref<IssueStatus>
@ -372,62 +366,6 @@ export class TMilestone extends TDoc implements Milestone {
declare space: Ref<Project>
}
/**
* @public
*/
@Model(tracker.class.Scrum, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Scrum, tracker.icon.Scrum)
export class TScrum extends TDoc implements Scrum {
@Prop(TypeString(), tracker.string.Title)
title!: string
@Prop(TypeMarkup(), tracker.string.Description)
description?: Markup
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(ArrOf(TypeRef(contact.class.Employee)), tracker.string.Members)
members!: Ref<Employee>[]
@Prop(Collection(tracker.class.Scrum), tracker.string.ScrumRecords)
scrumRecords?: number
@Prop(TypeDate(DateRangeMode.TIME), tracker.string.ScrumBeginTime)
beginTime!: Timestamp
@Prop(TypeDate(DateRangeMode.TIME), tracker.string.ScrumEndTime)
endTime!: Timestamp
declare space: Ref<Project>
}
/**
* @public
*/
@Model(tracker.class.ScrumRecord, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.ScrumRecord, tracker.icon.Scrum)
export class TScrumRecord extends TAttachedDoc implements ScrumRecord {
@Prop(TypeString(), tracker.string.Title)
label!: string
@Prop(TypeTimestamp(), tracker.string.ScrumBeginTime)
startTs!: Timestamp
@Prop(TypeTimestamp(), tracker.string.ScrumEndTime)
endTs?: Timestamp
@Prop(Collection(chunter.class.Comment), tracker.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), tracker.string.Attachments)
attachments!: number
declare attachedTo: Ref<Scrum>
declare space: Ref<Project>
declare scrumRecorder: Ref<EmployeeAccount>
}
@UX(core.string.Number)
@Model(tracker.class.TypeReportedTime, core.class.Type)
export class TTypeReportedTime extends TType {}
@ -441,8 +379,6 @@ export function createModel (builder: Builder): void {
TIssueStatus,
TTypeIssuePriority,
TMilestone,
TScrum,
TScrumRecord,
TTypeMilestoneStatus,
TTimeSpendReport,
TTypeReportedTime

View File

@ -13,56 +13,12 @@
// limitations under the License.
//
import core, {
Class,
DOMAIN_STATUS,
DOMAIN_TX,
Doc,
DocumentUpdate,
Ref,
SortingOrder,
StatusCategory,
TxCreateDoc,
TxOperations,
TxResult,
TxUpdateDoc,
generateId
} from '@hcengineering/core'
import core, { Ref, TxOperations, generateId } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags from '@hcengineering/tags'
import {
Issue,
IssueStatus,
IssueTemplate,
IssueTemplateChild,
Milestone,
MilestoneStatus,
Project,
TimeReportDayType,
calcRank,
createStatuses,
genRanks
} from '@hcengineering/tracker'
import { DOMAIN_TRACKER } from '.'
import { IssueStatus, Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
import tracker from './plugin'
enum DeprecatedIssueStatus {
Backlog,
Todo,
InProgress,
Done,
Canceled
}
const categoryByDeprecatedIssueStatus = {
[DeprecatedIssueStatus.Backlog]: tracker.issueStatusCategory.Backlog,
[DeprecatedIssueStatus.Todo]: tracker.issueStatusCategory.Unstarted,
[DeprecatedIssueStatus.InProgress]: tracker.issueStatusCategory.Started,
[DeprecatedIssueStatus.Done]: tracker.issueStatusCategory.Completed,
[DeprecatedIssueStatus.Canceled]: tracker.issueStatusCategory.Canceled
} as const
async function createDefaultProject (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Project, {
_id: tracker.project.DefaultProject
@ -87,7 +43,6 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
archived: false,
identifier: 'TSK',
sequence: 0,
issueStatuses: 0,
defaultIssueStatus: defaultStatusId,
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
defaultAssignee: undefined
@ -104,173 +59,6 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
}
}
async function fixProjectIssueStatusesOrder (tx: TxOperations, project: Project): Promise<TxResult> {
const statuses = await tx.findAll(
tracker.class.IssueStatus,
{ space: project._id },
{ lookup: { category: core.class.StatusCategory } }
)
statuses.sort((a, b) => (a.$lookup?.category?.order ?? 0) - (b.$lookup?.category?.order ?? 0))
const issueStatusRanks = genRanks(statuses.length)
return statuses.map((status) => {
const rank = issueStatusRanks.next().value
if (rank === undefined || status.rank === rank) return undefined
return tx.update(status, { rank })
})
}
async function fixProjectsIssueStatusesOrder (tx: TxOperations): Promise<void> {
const projects = await tx.findAll(tracker.class.Project, {})
await Promise.all(projects.map((project) => fixProjectIssueStatusesOrder(tx, project)))
}
async function upgradeProjectSettings (tx: TxOperations): Promise<void> {
const projects = await tx.findAll(tracker.class.Project, {
defaultTimeReportDay: { $exists: false }
})
await Promise.all(
projects.map((project) =>
tx.update(project, {
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay
})
)
)
}
async function upgradeProjectIssueStatuses (tx: TxOperations): Promise<void> {
const projects = await tx.findAll(tracker.class.Project, { issueStatuses: undefined })
if (projects.length > 0) {
for (const project of projects) {
const defaultStatusId: Ref<IssueStatus> = generateId()
await tx.update(project, { issueStatuses: 0, defaultIssueStatus: defaultStatusId })
await createStatuses(tx, project._id, tracker.class.IssueStatus, tracker.attribute.IssueStatus, defaultStatusId)
}
}
}
async function upgradeIssueStatuses (tx: TxOperations): Promise<void> {
const deprecatedStatuses = [
DeprecatedIssueStatus.Backlog,
DeprecatedIssueStatus.Canceled,
DeprecatedIssueStatus.Done,
DeprecatedIssueStatus.InProgress,
DeprecatedIssueStatus.Todo
]
const issues = await tx.findAll(tracker.class.Issue, { status: { $in: deprecatedStatuses as any } })
if (issues.length > 0) {
const statusByDeprecatedStatus = new Map<DeprecatedIssueStatus, Ref<IssueStatus>>()
for (const issue of issues) {
const deprecatedStatus = issue.status as unknown as DeprecatedIssueStatus
if (!statusByDeprecatedStatus.has(deprecatedStatus)) {
const category = categoryByDeprecatedIssueStatus[deprecatedStatus]
const issueStatus = await tx.findOne(tracker.class.IssueStatus, { category })
if (issueStatus === undefined) {
throw new Error(`Could not find a new status for "${DeprecatedIssueStatus[deprecatedStatus]}"`)
}
statusByDeprecatedStatus.set(deprecatedStatus, issueStatus._id)
}
await tx.update(issue, { status: statusByDeprecatedStatus.get(deprecatedStatus) })
}
}
}
async function migrateParentIssues (client: MigrationClient): Promise<void> {
let { updated } = await client.update(
DOMAIN_TRACKER,
{ _class: tracker.class.Issue, attachedToClass: { $exists: false } },
{
subIssues: 0,
collection: 'subIssues',
attachedToClass: tracker.class.Issue
}
)
updated += (
await client.update(
DOMAIN_TRACKER,
{ _class: tracker.class.Issue, parentIssue: { $exists: true } },
{ $rename: { parentIssue: 'attachedTo' } }
)
).updated
updated += (
await client.update(
DOMAIN_TRACKER,
{ _class: tracker.class.Issue, attachedTo: { $in: [null, undefined] } },
{ attachedTo: tracker.ids.NoParent }
)
).updated
if (updated === 0) {
return
}
const childrenCountById = new Map<Ref<Doc>, number>()
const parentIssueIds = (
await client.find<Issue>(DOMAIN_TRACKER, {
_class: tracker.class.Issue,
attachedTo: { $nin: [tracker.ids.NoParent] }
})
).map((issue) => issue.attachedTo)
for (const issueId of parentIssueIds) {
const count = childrenCountById.get(issueId) ?? 0
childrenCountById.set(issueId, count + 1)
}
for (const [_id, childrenCount] of childrenCountById) {
await client.update(DOMAIN_TRACKER, { _id }, { subIssues: childrenCount })
}
}
async function updateIssueParentInfo (client: MigrationClient, parentIssue: Issue | null): Promise<void> {
const parents =
parentIssue === null ? [] : [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
const migrationResult = await client.update<Issue>(
DOMAIN_TRACKER,
{
_class: tracker.class.Issue,
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
parents: { $exists: false }
},
{ parents }
)
if (migrationResult.matched > 0) {
const subIssues = await client.find<Issue>(DOMAIN_TRACKER, {
_class: tracker.class.Issue,
attachedTo: parentIssue?._id ?? tracker.ids.NoParent,
subIssues: { $gt: 0 }
})
for (const issue of subIssues) {
await updateIssueParentInfo(client, issue)
}
}
}
async function migrateIssueParentInfo (client: MigrationClient): Promise<void> {
await updateIssueParentInfo(client, null)
}
async function migrateIssueComponents (client: MigrationClient): Promise<void> {
const issues = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue, component: { $exists: false } })
if (issues.length === 0) {
return
}
for (const issue of issues) {
await client.update(DOMAIN_TRACKER, { _id: issue._id }, { component: null })
}
}
async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultProject(tx)
await createOrUpdate(
@ -288,617 +76,10 @@ async function createDefaults (tx: TxOperations): Promise<void> {
)
}
async function fillRank (client: MigrationClient): Promise<void> {
const docs = await client.find<Issue>(DOMAIN_TRACKER, {
_class: tracker.class.Issue,
rank: ''
})
let last = (
await client.find<Issue>(
DOMAIN_TRACKER,
{
_class: tracker.class.Issue,
rank: { $ne: '' }
},
{
sort: { rank: SortingOrder.Descending },
limit: 1
}
)
)[0]
for (const doc of docs) {
const rank = calcRank(last)
await client.update(
DOMAIN_TRACKER,
{
_id: doc._id
},
{
rank
}
)
await client.update(
DOMAIN_TX,
{ 'tx.objectId': doc._id, 'tx._class': core.class.TxCreateDoc },
{ 'tx.attributes.rank': rank }
)
doc.rank = rank
last = doc
}
}
async function upgradeProjects (tx: TxOperations): Promise<void> {
await upgradeProjectIssueStatuses(tx)
await fixProjectsIssueStatusesOrder(tx)
await upgradeProjectSettings(tx)
}
async function upgradeIssues (tx: TxOperations): Promise<void> {
await upgradeIssueStatuses(tx)
const issues = await tx.findAll(tracker.class.Issue, {
$or: [{ blockedBy: { $exists: true } }, { relatedIssue: { $exists: true } }]
})
for (const i of issues) {
const rel = (i as any).relatedIssue as Ref<Issue>[]
const upd: DocumentUpdate<Issue> = {}
if (rel != null) {
;(upd as any).relatedIssue = null
upd.relations = rel.map((it) => ({ _id: it, _class: tracker.class.Issue }))
}
if (i.blockedBy !== undefined) {
if ((i.blockedBy as any[]).find((it) => typeof it === 'string') !== undefined) {
upd.blockedBy = (i.blockedBy as unknown as Ref<Issue>[]).map((it) => ({ _id: it, _class: tracker.class.Issue }))
}
}
if (Object.keys(upd).length > 0) {
await tx.update(i, upd)
}
}
}
async function renameSprintToMilestone (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_TRACKER,
{
_class: tracker.class.Issue,
sprint: { $exists: true }
},
{
$rename: { sprint: 'milestone' }
}
)
await client.update(
DOMAIN_TRACKER,
{
_class: 'tracker:class:Sprint' as Ref<Class<Doc>>
},
{
_class: tracker.class.Milestone
}
)
const milestones = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Milestone })
for (const milestone of milestones) {
await client.update(
DOMAIN_TX,
{
objectId: milestone._id,
objectClass: 'tracker:class:Sprint' as Ref<Class<Doc>>
},
{
objectClass: tracker.class.Milestone
}
)
}
await client.update(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc,
'tx.objectClass': tracker.class.Issue,
'tx.attributes.sprint': { $exists: true }
},
{
$rename: { 'tx.attributes.sprint': 'tx.attributes.milestone' }
}
)
await client.update(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxUpdateDoc,
'tx.objectClass': tracker.class.Issue,
'tx.operations.sprint': { $exists: true }
},
{
$rename: { 'tx.operations.sprint': 'tx.operations.milestone' }
}
)
await client.update(
DOMAIN_TX,
{
objectClass: tracker.class.Issue,
_class: core.class.TxUpdateDoc,
'operations.sprint': { $exists: true }
},
{
$rename: { 'operations.sprint': 'operations.milestone' }
}
)
const templates = await client.find<IssueTemplate>(DOMAIN_TRACKER, {
_class: tracker.class.IssueTemplate,
sprint: { $exists: true }
})
for (const template of templates) {
const children: IssueTemplateChild[] = template.children.map((p) => {
const res = {
...p,
milestone: p.milestone
}
delete (res as any).sprint
return res
})
await client.update<IssueTemplate>(
DOMAIN_TRACKER,
{
_id: template._id
},
{
children
}
)
await client.update(
DOMAIN_TRACKER,
{
_id: template._id
},
{
$rename: { sprint: 'milestone' }
}
)
const createTxes = await client.find<TxCreateDoc<IssueTemplate>>(DOMAIN_TX, {
objectId: template._id,
_class: core.class.TxCreateDoc
})
for (const createTx of createTxes) {
const children: IssueTemplateChild[] = createTx.attributes.children.map((p) => {
const res = {
...p,
milestone: p.milestone
}
delete (res as any).sprint
return res
})
await client.update<TxCreateDoc<IssueTemplate>>(
DOMAIN_TX,
{
_id: createTx._id
},
{
children
}
)
await client.update(
DOMAIN_TX,
{
_id: createTx._id
},
{
$rename: { 'attributes.sprint': 'attributes.milestone' }
}
)
}
const updateTxes = await client.find<TxUpdateDoc<IssueTemplate>>(DOMAIN_TX, {
objectId: template._id,
_class: core.class.TxUpdateDoc
})
for (const updateTx of updateTxes) {
if ((updateTx.operations as any).sprint !== undefined) {
await client.update(
DOMAIN_TX,
{
_id: updateTx._id
},
{
$rename: { 'operations.sprint': 'operations.milestone' }
}
)
}
if (updateTx.operations.children !== undefined) {
const children: IssueTemplateChild[] = updateTx.operations.children.map((p) => {
const res = {
...p,
milestone: p.milestone
}
delete (res as any).sprint
return res
})
await client.update(
DOMAIN_TX,
{
_id: updateTx._id
},
{
children
}
)
}
}
}
}
async function renameProject (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_TRACKER,
{
_class: { $in: [tracker.class.Issue, tracker.class.Milestone] },
project: { $exists: true }
},
{
$rename: { project: 'component' }
}
)
await client.update(
DOMAIN_TRACKER,
{
_class: tracker.class.Project
},
{
_class: tracker.class.Component
}
)
const components = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Component })
for (const component of components) {
await client.update(
DOMAIN_TX,
{
objectId: component._id,
objectClass: tracker.class.Project
},
{
objectClass: tracker.class.Component
}
)
}
await client.update(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc,
'tx.objectClass': tracker.class.Issue,
'tx.attributes.project': { $exists: true }
},
{
$rename: { 'tx.attributes.project': 'tx.attributes.component' }
}
)
await client.update(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxUpdateDoc,
'tx.objectClass': tracker.class.Issue,
'tx.operations.project': { $exists: true }
},
{
$rename: { 'tx.operations.project': 'tx.operations.component' }
}
)
await client.update(
DOMAIN_TX,
{
objectClass: tracker.class.Milestone,
_class: core.class.TxCreateDoc,
'attributes.project': { $exists: true }
},
{
$rename: { 'attributes.project': 'attributes.component' }
}
)
await client.update(
DOMAIN_TX,
{
objectClass: { $in: [tracker.class.Issue, tracker.class.Milestone] },
_class: core.class.TxUpdateDoc,
'operations.project': { $exists: true }
},
{
$rename: { 'operations.project': 'operations.component' }
}
)
const templates = await client.find<IssueTemplate>(DOMAIN_TRACKER, {
_class: tracker.class.IssueTemplate,
project: { $exists: true }
})
for (const template of templates) {
const children: IssueTemplateChild[] = template.children.map((p) => {
const res = {
...p,
component: p.component
}
delete (res as any).project
return res
})
await client.update<IssueTemplate>(
DOMAIN_TRACKER,
{
_id: template._id
},
{
children
}
)
await client.update(
DOMAIN_TRACKER,
{
_id: template._id
},
{
$rename: { project: 'component' }
}
)
const createTxes = await client.find<TxCreateDoc<IssueTemplate>>(DOMAIN_TX, {
objectId: template._id,
_class: core.class.TxCreateDoc
})
for (const createTx of createTxes) {
const children: IssueTemplateChild[] = createTx.attributes.children.map((p) => {
const res = {
...p,
component: p.component
}
delete (res as any).project
return res
})
await client.update<TxCreateDoc<IssueTemplate>>(
DOMAIN_TX,
{
_id: createTx._id
},
{
children
}
)
await client.update(
DOMAIN_TX,
{
_id: createTx._id
},
{
$rename: { 'attributes.project': 'attributes.component' }
}
)
}
const updateTxes = await client.find<TxUpdateDoc<IssueTemplate>>(DOMAIN_TX, {
objectId: template._id,
_class: core.class.TxUpdateDoc
})
for (const updateTx of updateTxes) {
if ((updateTx.operations as any).project !== undefined) {
await client.update(
DOMAIN_TX,
{
_id: updateTx._id
},
{
$rename: { 'operations.project': 'operations.component' }
}
)
}
if (updateTx.operations.children !== undefined) {
const children: IssueTemplateChild[] = updateTx.operations.children.map((p) => {
const res = {
...p,
component: p.component
}
delete (res as any).project
return res
})
await client.update(
DOMAIN_TX,
{
_id: updateTx._id
},
{
children
}
)
}
}
}
const defaultSpace = (
await client.find<Project>(DOMAIN_SPACE, {
_id: 'tracker:team:DefaultTeam' as Ref<Project>
})
)[0]
if (defaultSpace !== undefined) {
await client.delete(DOMAIN_SPACE, tracker.project.DefaultProject)
await client.create(DOMAIN_SPACE, {
...defaultSpace,
_id: tracker.project.DefaultProject,
_class: tracker.class.Project,
description: defaultSpace.description === 'Default team' ? 'Default project' : defaultSpace.description
})
await client.delete(DOMAIN_SPACE, defaultSpace._id)
}
await client.update(
DOMAIN_SPACE,
{
_id: 'tracker:team:DefaultTeam' as Ref<Project>,
_class: 'tracker:class:Team' as Ref<Class<Doc>>
},
{
_id: tracker.project.DefaultProject,
_class: tracker.class.Project,
description: 'Default project'
}
)
await client.update(
DOMAIN_TRACKER,
{
attachedTo: 'tracker:team:DefaultTeam' as Ref<Doc>
},
{
attachedTo: tracker.project.DefaultProject
}
)
await client.update(
DOMAIN_TRACKER,
{
space: 'tracker:team:DefaultTeam' as Ref<Project>
},
{
space: tracker.project.DefaultProject
}
)
await client.update(
DOMAIN_TRACKER,
{
attachedToClass: 'tracker:class:Team' as Ref<Class<Doc>>
},
{
attachedToClass: tracker.class.Project
}
)
await client.update(
DOMAIN_TX,
{
objectId: 'tracker:team:DefaultTeam' as Ref<Project>
},
{
objectId: tracker.project.DefaultProject
}
)
await client.update(
DOMAIN_TX,
{
objectClass: 'tracker:class:Team' as Ref<Class<Doc>>
},
{
objectClass: tracker.class.Project
}
)
await client.update(
DOMAIN_TX,
{
'tx.objectClass': 'tracker:class:Team' as Ref<Class<Doc>>
},
{
'tx.objectClass': tracker.class.Project
}
)
await client.update(
DOMAIN_TX,
{
objectSpace: 'tracker:team:DefaultTeam' as Ref<Project>
},
{
objectSpace: tracker.project.DefaultProject
}
)
await client.update(
DOMAIN_TX,
{
'tx.objectSpace': 'tracker:team:DefaultTeam' as Ref<Project>
},
{
'tx.objectSpace': tracker.project.DefaultProject
}
)
}
async function fixMilestoneEmptyStatuses (client: MigrationClient): Promise<void> {
await client.update<Milestone>(
DOMAIN_TRACKER,
{ _class: tracker.class.Milestone, $or: [{ status: null }, { status: undefined }] },
{ status: MilestoneStatus.Planned }
)
}
async function removeExtraStatuses (client: TxOperations): Promise<void> {
const projects = await client.findAll(tracker.class.Project, {})
for (const project of projects) {
const projectStatuses = await client.findAll(tracker.class.IssueStatus, { space: project._id })
const statusesMap: Map<Ref<StatusCategory>, Map<string, IssueStatus[]>> = new Map()
for (const status of projectStatuses) {
if (status.category === undefined) continue
const map = statusesMap.get(status.category) ?? new Map<string, IssueStatus[]>()
const arr = map.get(status.name) ?? []
arr.push(status)
map.set(status.name, arr)
statusesMap.set(status.category, map)
}
for (const statuses of statusesMap.values()) {
for (const statusesArr of statuses.values()) {
if (statusesArr.length < 2) continue
const migrateTo = statusesArr[0]._id
for (let index = 1; index < statusesArr.length; index++) {
const status = statusesArr[index]
const tasks = await client.findAll(tracker.class.Issue, { status: status._id })
for (const task of tasks) {
await client.update(task, { status: migrateTo })
}
}
}
}
}
}
export const trackerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_TRACKER,
{ _class: tracker.class.Issue, reports: { $exists: false } },
{
reports: 0,
estimation: 0,
reportedTime: 0
}
)
await Promise.all([migrateIssueComponents(client), migrateParentIssues(client)])
await migrateIssueParentInfo(client)
await fillRank(client)
await renameSprintToMilestone(client)
await renameProject(client)
// Move all status objects into status domain
await client.move(
DOMAIN_TRACKER,
{
_class: tracker.class.IssueStatus
},
DOMAIN_STATUS
)
await client.update(
DOMAIN_STATUS,
{ _class: tracker.class.IssueStatus, ofAttribute: { $exists: false } },
{
ofAttribute: tracker.attribute.IssueStatus
}
)
await fixMilestoneEmptyStatuses(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)
await upgradeProjects(tx)
await upgradeIssues(tx)
await removeExtraStatuses(tx)
}
}

View File

@ -13,152 +13,9 @@
// limitations under the License.
//
import core, { AnyAttribute, DOMAIN_TX, Ref, TxCreateDoc, TxCUD, TxProcessor, TxRemoveDoc } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
import { BuildModelKey, FilteredView, Viewlet, ViewletPreference } from '@hcengineering/view'
import { DOMAIN_VIEW } from '.'
import view from './plugin'
async function migrateViewletPreference (client: MigrationClient): Promise<void> {
const targets: Record<string, string[]> = {
'inventory:viewlet:TableProduct': ['attachedTo'],
'lead:viewlet:TableCustomer': ['_class'],
'lead:viewlet:TableLead': ['attachedTo', 'state', 'doneState'],
'recruit.viewlet.TableApplicant': ['attachedTo', 'assignee', 'state', 'doneState'],
'task.viewlet.TableIssue': ['assignee', 'state', 'doneState']
}
for (const target in targets) {
const keys = targets[target]
const preferences = await client.find<ViewletPreference>(DOMAIN_PREFERENCE, {
attachedTo: target as Ref<Viewlet>
})
for (const pref of preferences) {
let needUpdate = false
for (const key of keys) {
const index = pref.config.findIndex((p) => p === `$lookup.${key}`)
if (index !== -1) {
pref.config.splice(index, 1, key)
needUpdate = true
}
}
if (needUpdate) {
await client.update<ViewletPreference>(
DOMAIN_PREFERENCE,
{
_id: pref._id
},
{
config: pref.config
}
)
}
}
}
}
async function migrateSavedFilters (client: MigrationClient): Promise<void> {
try {
await client.move(
DOMAIN_PREFERENCE,
{
_class: view.class.FilteredView
},
DOMAIN_VIEW
)
} catch (err: any) {
console.log(err)
}
const preferences = await client.find<FilteredView>(DOMAIN_VIEW, {
_class: view.class.FilteredView,
users: { $exists: false }
})
for (const pref of preferences) {
await client.update<FilteredView>(
DOMAIN_VIEW,
{
_id: pref._id
},
{
users: [pref.createdBy]
}
)
}
}
async function fixViewletPreferenceRemovedAttributes (client: MigrationClient): Promise<void> {
const removeTxes = await client.find<TxRemoveDoc<AnyAttribute>>(DOMAIN_TX, {
_class: core.class.TxRemoveDoc,
objectClass: core.class.Attribute
})
for (const removeTx of removeTxes) {
const createTx = (
await client.find<TxCreateDoc<AnyAttribute>>(DOMAIN_TX, {
_class: core.class.TxCreateDoc,
objectId: removeTx.objectId
})
)[0]
const key = createTx.attributes.name
await client.update<ViewletPreference>(
DOMAIN_PREFERENCE,
{ config: key },
{
$pull: { config: key }
}
)
}
}
async function fixPreferenceObjectKey (client: MigrationClient): Promise<void> {
const preferences = await client.find<ViewletPreference>(DOMAIN_PREFERENCE, { _class: view.class.ViewletPreference })
for (const preference of preferences) {
let index = preference.config.indexOf('')
if (index === -1) continue
index = preference.config.indexOf('', index + 1)
if (index === -1) continue
const descTxes = await client.find<TxCUD<Viewlet>>(DOMAIN_TX, { objectId: preference.attachedTo })
const desc = TxProcessor.buildDoc2Doc<Viewlet>(descTxes)
if (desc === undefined) continue
const targets = desc.config.filter((p) => (p as BuildModelKey).key === '')
let i = 0
while (index !== -1) {
const target = targets[i++]
if (target !== undefined) {
await client.update(
DOMAIN_PREFERENCE,
{
_id: preference._id
},
{ $set: { [`config.${index}`]: target } }
)
} else {
await client.update(
DOMAIN_PREFERENCE,
{
_id: preference._id
},
{ $unset: { [`config.${index}`]: 1 } }
)
await client.update(
DOMAIN_PREFERENCE,
{
_id: preference._id
},
{ $pull: { config: null } }
)
}
index = preference.config.indexOf('', index + 1)
}
}
}
export const viewOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await migrateViewletPreference(client)
await migrateSavedFilters(client)
await fixViewletPreferenceRemovedAttributes(client)
await fixPreferenceObjectKey(client)
},
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
}

View File

@ -216,26 +216,6 @@
"MoveAndDeleteMilestone": "Move Issues to {newMilestone} and Delete {deleteMilestone}",
"MoveAndDeleteMilestoneConfirm": "Do you want to delete milestone and move issues to another milestone?",
"Scrums": "Scrums",
"Scrum": "Scrum",
"ScrumMembersTitle": "Scrum members",
"ScrumMembersSearchPlaceholder": "Change scrum members\u2026",
"ScrumBeginTime": "Scrum begin time",
"ScrumEndTime": "Scrum end time",
"NewScrum": "New scrum",
"CreateScrum": "Create scrum",
"ScrumTitlePlaceholder": "Scrum title",
"ScrumDescriptionPlaceholder": "Add scrum description",
"ScrumRecords": "Scrum records",
"ScrumRecord": "Scrum record",
"StartRecord": "Start recording",
"StopRecord": "Stop recording",
"ChangeScrumRecord": "Start recording another scrum",
"ChangeScrumRecordConfirm": "Do you want to stop recording {previousRecord} and start recording {newRecord}?",
"ScrumRecorder": "Scrum recorder",
"ScrumRecordTimeReports": "Recorded time reports",
"ScrumRecordObjects": "Changed objects",
"Estimation": "Estimation",
"ReportedTime": "Reported Time",
"TimeSpendReports": "Time spend reports",

View File

@ -216,26 +216,6 @@
"MoveAndDeleteMilestone": "Переместить Задачи в {newMilestone} и Удалить {deleteMilestone}",
"MoveAndDeleteMilestoneConfirm": "Вы действительно хотите удалить этап и перенести задачи в другой?",
"Scrums": "Скрамы",
"Scrum": "Скрам",
"ScrumMembersTitle": "Участники скрама",
"ScrumMembersSearchPlaceholder": "Изменить участников скрама\u2026",
"ScrumBeginTime": "Время начала скрама",
"ScrumEndTime": "Время конца скрама",
"NewScrum": "Новый скрам",
"CreateScrum": "Создать скрам",
"ScrumTitlePlaceholder": "Название скрама",
"ScrumDescriptionPlaceholder": "Описание скрама",
"ScrumRecords": "Записи скрамов",
"ScrumRecord": "Запись скрама",
"StartRecord": "Начать запись",
"StopRecord": " Закончить запись",
"ChangeScrumRecord": "Начать запись другого скрама",
"ChangeScrumRecordConfirm": "Вы действительно хотите прекратить запись {previousScrumRecord} и начать записывать {newScrumRecord}?",
"ScrumRecorder": "Ведущий скрама",
"ScrumRecordTimeReports": "Временные отчеты",
"ScrumRecordObjects": "Измененные объекты",
"Estimation": "Оценка",
"ReportedTime": "Использовано",
"TimeSpendReports": "Отчеты по времени",

View File

@ -38,7 +38,6 @@ loadMetadata(tracker.icon, {
Parent: `${icons}#parent-issue`, // TODO: add icon
Milestone: `${icons}#milestone`,
IssueTemplates: `${icons}#issuetemplates`,
Scrum: `${icons}#scrum`,
Start: `${icons}#start`,
Stop: `${icons}#stop`,

View File

@ -84,7 +84,6 @@
archived: false,
identifier,
sequence: 0,
issueStatuses: 0,
defaultIssueStatus: defaultStatusId,
defaultAssignee: defaultAssignee ?? undefined,
icon,
@ -98,7 +97,7 @@
return
}
const { sequence, issueStatuses, defaultIssueStatus, ...projectData } = getProjectData()
const { sequence, defaultIssueStatus, ...projectData } = getProjectData()
const update: DocumentUpdate<Project> = {}
if (projectData.name !== project?.name) {
update.name = projectData.name

View File

@ -1,41 +0,0 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Doc, TxCUD } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { AttributeModel } from '@hcengineering/view'
export let value: TxCUD<Doc>
export let onNavigate: () => void | undefined
const query = createQuery()
const client = getClient()
let presenter: AttributeModel | undefined
let doc: Doc | undefined
$: query.query(value.objectClass, { _id: value.objectId }, (res) => {
doc = res.shift()
})
$: getObjectPresenter(client, value.objectClass, { key: '' }).then((p) => {
presenter = p
})
</script>
{#if doc && presenter}
<svelte:component this={presenter.presenter} value={doc} onClick={onNavigate} shouldShowAvatar noUnderline />
{/if}

View File

@ -1,118 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Data, DateRangeMode, generateId, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
import { UserBoxList } from '@hcengineering/contact-resources'
import { Scrum, Project } from '@hcengineering/tracker'
import { DateRangePresenter, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { StyledTextArea } from '@hcengineering/text-editor'
import ProjectPresenter from '../projects/ProjectPresenter.svelte'
export let space: Ref<Project>
const objectId: Ref<Scrum> = generateId()
const dispatch = createEventDispatcher()
const client = getClient()
const object: Partial<Data<Scrum>> = {
title: '' as IntlString,
description: '',
members: [],
attachments: 0,
scrumRecords: 0
}
let canSave = false
async function onSave () {
if (object.beginTime && object.endTime) {
await client.createDoc(tracker.class.Scrum, space, object as Data<Scrum>, objectId)
}
}
$: {
if (
object.endTime &&
object.beginTime &&
object.endTime - object.beginTime > 0 &&
object.title !== '' &&
object.members?.length !== 0
) {
canSave = true
} else {
canSave = false
}
}
</script>
<Card
label={tracker.string.NewScrum}
okLabel={tracker.string.CreateScrum}
{canSave}
okAction={onSave}
gap={'gapV-4'}
on:close={() => dispatch('close')}
on:changeContent
>
<svelte:fragment slot="header">
<SpaceSelector
_class={tracker.class.Project}
label={tracker.string.Project}
bind:space
component={ProjectPresenter}
iconWithEmojii={tracker.component.IconWithEmojii}
defaultIcon={tracker.icon.Home}
/>
</svelte:fragment>
<EditBox
bind:value={object.title}
placeholder={tracker.string.ScrumTitlePlaceholder}
kind={'large-style'}
autoFocus
/>
<StyledTextArea
bind:content={object.description}
placeholder={tracker.string.ScrumDescriptionPlaceholder}
kind={'emphasized'}
/>
<svelte:fragment slot="pool">
<UserBoxList bind:items={object.members} label={tracker.string.ScrumMembersSearchPlaceholder} />
<DateRangePresenter
value={object.beginTime}
labelNull={tracker.string.ScrumBeginTime}
mode={DateRangeMode.TIME}
on:change={(res) => {
if (res.detail !== undefined && res.detail !== null) {
object.beginTime = res.detail
}
}}
editable
/>
<DateRangePresenter
value={object.endTime}
labelNull={tracker.string.ScrumEndTime}
mode={DateRangeMode.TIME}
on:change={(res) => {
if (res.detail !== undefined && res.detail !== null) {
object.endTime = res.detail
}
}}
editable
/>
</svelte:fragment>
</Card>

View File

@ -1,22 +0,0 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { Button, IconStart, IconStop } from '@hcengineering/ui'
import { handleRecordingScrum } from '../..'
import tracker from '../../plugin'
export let scrum: Scrum
export let activeScrumRecord: ScrumRecord | undefined
const client = getClient()
$: isRecording = scrum._id === activeScrumRecord?.attachedTo
</script>
<Button
size="small"
icon={isRecording ? IconStop : IconStart}
label={isRecording ? tracker.string.StopRecord : tracker.string.StartRecord}
kind={'primary'}
on:click={() => handleRecordingScrum(client, scrum, activeScrumRecord)}
/>

View File

@ -1,38 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { Button } from '@hcengineering/ui'
import { handleRecordingScrum } from '../..'
import tracker from '../../plugin'
export let value: Scrum
export let activeScrumRecord: ScrumRecord | undefined
let title: string
const client = getClient()
$: isRecording = value._id === activeScrumRecord?.attachedTo
$: translate(isRecording ? tracker.string.StopRecord : tracker.string.StartRecord, {}).then((res) => (title = res))
</script>
<Button
kind="link"
justify="center"
{title}
icon={isRecording ? tracker.icon.Stop : tracker.icon.Start}
on:click={async () => handleRecordingScrum(client, value, activeScrumRecord)}
/>

View File

@ -1,59 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { DateRangeMode } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Scrum } from '@hcengineering/tracker'
import { DateRangePresenter } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: Scrum
const client = getClient()
$: beginTime = value.beginTime
$: endTime = value.endTime
const updateObject = (fields: Partial<Scrum> | undefined) => {
if (fields) {
client.update(value, fields)
}
}
</script>
<DateRangePresenter
value={beginTime}
mode={DateRangeMode.TIME}
labelNull={tracker.string.ScrumBeginTime}
on:change={(res) => {
if (res.detail !== null && res.detail !== undefined && res.detail < endTime) {
updateObject({ beginTime: res.detail })
}
}}
noShift
editable
/>
<DateRangePresenter
value={endTime}
mode={DateRangeMode.TIME}
labelNull={tracker.string.ScrumEndTime}
on:change={(res) => {
if (res.detail !== null && res.detail !== undefined && res.detail > beginTime) {
updateObject({ endTime: res.detail })
}
}}
noShift
editable
/>

View File

@ -1,48 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import type { Scrum } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
import { DocAttributeBar } from '@hcengineering/view-resources'
import tracker from '../../plugin'
export let scrum: Scrum
const client = getClient()
async function change (field: string, value: any) {
await client.update(scrum, { [field]: value })
}
</script>
<div class="popupPanel-body__aside flex shown">
<div class="p-4 w-60 left-divider">
<div class="fs-title text-xl">
<EditBox bind:value={scrum.title} on:change={() => scrum.title && change('title', scrum.title)} />
</div>
<div class="mt-2">
<StyledTextBox
alwaysEdit
showButtons={false}
placeholder={tracker.string.Description}
content={scrum.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<DocAttributeBar object={scrum} ignoreKeys={['title', 'description']} />
</div>
</div>

View File

@ -1,71 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { SortingOrder, WithLookup } from '@hcengineering/core'
import { Scrum } from '@hcengineering/tracker'
import {
Button,
Icon,
deviceOptionsStore as deviceInfo,
getCurrentResolvedLocation,
navigate,
showPopup
} from '@hcengineering/ui'
import tracker from '../../plugin'
import Expanded from '../icons/Expanded.svelte'
import ScrumPopup from './ScrumPopup.svelte'
export let scrum: WithLookup<Scrum>
let container: HTMLElement
$: twoRows = $deviceInfo.twoRows
const handleSelectScrum = (evt: MouseEvent): void => {
showPopup(
ScrumPopup,
{
_class: tracker.class.Scrum,
query: { space: scrum.space },
options: { sort: { beginTime: SortingOrder.Ascending } }
},
container,
(value) => {
if (value != null) {
const loc = getCurrentResolvedLocation()
loc.path[5] = value._id
navigate(loc)
}
}
)
}
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
<div class:ac-header-full={!twoRows} class:flex-between={twoRows}>
<div bind:this={container} class="ac-header__wrap-title mr-3">
<Button size={'small'} kind={'link'} on:click={handleSelectScrum}>
<svelte:fragment slot="content">
<div class="ac-header__icon">
<Icon icon={tracker.icon.Scrum} size={'small'} />
</div>
<span class="ac-header__title mr-1">{scrum.title}</span>
<Icon icon={Expanded} size={'small'} />
</svelte:fragment>
</Button>
</div>
</div>
<slot name="options" />
</div>

View File

@ -1,43 +0,0 @@
<!--
// Copyright © 2023 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { ObjectPopup } from '@hcengineering/presentation'
import { Scrum } from '@hcengineering/tracker'
import ScrumTitle from './ScrumTitle.svelte'
import tracker from '../../plugin'
export let selected: Ref<Scrum> | undefined = undefined
export let query: DocumentQuery<Scrum> = {}
export let options: FindOptions<Scrum> = {}
</script>
<ObjectPopup
_class={tracker.class.Scrum}
{selected}
bind:docQuery={query}
{options}
searchField="title"
multiSelect={false}
allowDeselect={false}
closeAfterSelect
shadows
on:update
on:close
>
<svelte:fragment slot="item" let:item={scrum}>
<ScrumTitle value={scrum} />
</svelte:fragment>
</ObjectPopup>

View File

@ -1,43 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Scrum } from '@hcengineering/tracker'
import { getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
export let value: Scrum
function navigateToScrum () {
const loc = getCurrentResolvedLocation()
loc.path[5] = value._id
loc.path.length = 6
navigate(loc)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={navigateToScrum}>
<span title={value.title} class="scrumLabel flex-grow">{value.title}</span>
</div>
<style lang="scss">
.scrumLabel {
display: block;
min-width: 0;
text-align: left;
color: var(--theme-caption-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@ -1,54 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Button, deviceOptionsStore as deviceInfo, Icon, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import Expanded from '../icons/Expanded.svelte'
import { WithLookup } from '@hcengineering/core'
import { ScrumRecord } from '@hcengineering/tracker'
import ScrumRecordPopup from './ScrumRecordPopup.svelte'
import ScrumRecordTitlePresenter from './ScrumRecordTitlePresenter.svelte'
export let scrumRecord: WithLookup<ScrumRecord>
let container: HTMLElement
const dispatch = createEventDispatcher()
$: twoRows = $deviceInfo.twoRows
const handleSelectScrumRecord = (evt: MouseEvent): void => {
showPopup(ScrumRecordPopup, { query: { attachedTo: scrumRecord.attachedTo } }, container, (value) => {
if (value != null) {
scrumRecord = value
dispatch('scrumRecord', scrumRecord._id)
}
})
}
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
<div class:ac-header-full={!twoRows} class:flex-between={twoRows}>
<div bind:this={container} class="ac-header__wrap-title mr-3">
<Button size={'small'} kind={'link'} on:click={handleSelectScrumRecord}>
<svelte:fragment slot="content">
<ScrumRecordTitlePresenter value={scrumRecord} />
<Icon icon={Expanded} size={'small'} />
</svelte:fragment>
</Button>
</div>
</div>
<slot name="options" />
</div>

View File

@ -1,68 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { DateRangeMode, WithLookup } from '@hcengineering/core'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { DateRangePresenter, Label } from '@hcengineering/ui'
import tracker from '../../plugin'
import { EmployeeRefPresenter } from '@hcengineering/contact-resources'
export let scrumRecord: WithLookup<ScrumRecord>
export let scrum: Scrum
</script>
<div class="content">
<span class="label fs-bold">
<Label label={tracker.string.ScrumRecorder} />
</span>
<EmployeeRefPresenter value={scrumRecord.$lookup?.scrumRecorder?.employee} />
<span class="label fs-bold">
<Label label={tracker.string.ScrumBeginTime} />
</span>
<DateRangePresenter value={scrumRecord.startTs} mode={DateRangeMode.DATETIME} kind="link" editable={false} />
{#if scrumRecord.endTs}
<span class="label fs-bold">
<Label label={tracker.string.ScrumEndTime} />
</span>
<DateRangePresenter value={scrumRecord.endTs} mode={DateRangeMode.DATETIME} kind="link" editable={false} />
{/if}
<span class="label fs-bold">
<Label label={tracker.string.Scrum} />
</span>
<span class="fs-bold scrumTitle">
{scrum.title}
</span>
</div>
<style lang="scss">
.content {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-flow: row;
justify-content: start;
align-items: center;
gap: 1rem;
margin-top: 1rem;
width: 100%;
height: min-content;
}
.scrumTitle {
color: var(--accent-color);
}
</style>

View File

@ -1,111 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { AttachedDoc, Doc, SortingOrder, TxCollectionCUD, TxCUD } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { groupBy, List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import ChangedObjectPresenter from './ChangedObjectPresenter.svelte'
export let txes: TxCUD<Doc>[]
export let onNavigate: () => void
const TRACKED_OBJECTS = [
tracker.class.Issue,
tracker.class.IssueTemplate,
tracker.class.Component,
tracker.class.Milestone
] as const
let changedObjectTxes: TxCUD<Doc>[] = []
// Drop RemoveDoc Txes and filter by supported tracked objects
$: filteredTxesCU = txes
.filter((tx) => TRACKED_OBJECTS.includes(tx.objectClass))
.filter((tx) => {
if (tx.objectClass === tracker.class.Issue) {
const issueTx = tx as TxCollectionCUD<Issue, AttachedDoc>
return issueTx.tx.objectClass !== tracker.class.Issue || issueTx.tx._class !== core.class.TxRemoveDoc
}
return tx._class !== core.class.TxRemoveDoc
})
// Convert Issue txes to common model
$: objectTxes = filteredTxesCU.map((tx) => {
const objectTx = { ...tx }
if (tx.objectClass === tracker.class.Issue) {
const issueTx = tx as TxCollectionCUD<Issue, AttachedDoc>
if (issueTx.tx.objectClass === tracker.class.Issue) {
objectTx.objectId = issueTx.tx.objectId
objectTx._class = issueTx.tx._class
} else {
objectTx._class = core.class.TxUpdateDoc
}
}
return objectTx
})
// Retrieve single txes for changed objects: TxCreateDoc if it exist for object, else last TxUpdateDoc
$: {
changedObjectTxes = []
const txesByObjectId = groupBy(objectTxes, 'objectId')
Object.values(txesByObjectId).forEach((objectTxes) => {
let objectTx: TxCUD<Doc> | undefined
objectTxes.forEach((tx) => {
const currentTx = tx as TxCUD<Doc>
if (
!objectTx ||
currentTx._class === core.class.TxCreateDoc ||
(objectTx._class === core.class.TxUpdateDoc && objectTx.modifiedOn < currentTx.modifiedOn)
) {
objectTx = currentTx
}
})
if (objectTx) {
changedObjectTxes.push(objectTx)
}
})
changedObjectTxes = changedObjectTxes.sort((leftObjectTx, rightObjectTx) =>
leftObjectTx.objectClass.localeCompare(rightObjectTx.objectClass)
)
}
</script>
<List
_class={core.class.TxCUD}
documents={changedObjectTxes}
config={[
{
key: '',
presenter: ChangedObjectPresenter,
props: { onNavigate }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: {}
}
]}
viewOptions={{ orderBy: ['modifiedOn', SortingOrder.Descending], groupBy: [] }}
disableHeader
/>

View File

@ -1,150 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { Class, Doc, Ref, SortingOrder, TxCUD, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { ParentsNavigator, UpDownNavigator } from '@hcengineering/view-resources'
import { Panel } from '@hcengineering/panel'
import { Button, closePanel, TabItem, TabList } from '@hcengineering/ui'
import tracker from '../../plugin'
import { handleRecordingScrum } from '../..'
import ScrumRecordInfo from './ScrumRecordInfo.svelte'
import contact from '@hcengineering/contact'
import ScrumRecordTimeSpend from './ScrumRecordTimeSpend.svelte'
import ScrumRecordObjects from './ScrumRecordObjects.svelte'
import { scrumRecordTitleMap, ScrumRecordViewMode } from '../../utils'
export let _id: Ref<ScrumRecord>
export let _class: Ref<Class<ScrumRecord>>
const scrumRecordQuery = createQuery()
const client = getClient()
const txQuery = createQuery()
const modeList: TabItem[] = Object.entries(scrumRecordTitleMap).map(([id, labelIntl]) => ({
id,
labelIntl,
action: () => handleViewModeChanged(id as ScrumRecordViewMode)
}))
let scrumRecord: WithLookup<ScrumRecord> | undefined
let scrum: Scrum | undefined
let isRecording = false
let txes: TxCUD<Doc>[] = []
let mode: ScrumRecordViewMode = 'timeReports'
const onNavigate = () => closePanel()
const handleViewModeChanged = (newMode: ScrumRecordViewMode) => {
if (newMode === undefined || newMode === mode) {
return
}
mode = newMode
}
$: _class &&
_id &&
scrumRecordQuery.query(
_class,
{ _id },
(result) => {
scrumRecord = result.shift()
},
{
lookup: {
attachedTo: tracker.class.Scrum,
scrumRecorder: contact.class.EmployeeAccount
}
}
)
$: scrum = scrumRecord?.$lookup?.attachedTo
$: {
if (scrumRecord?.startTs && !scrumRecord.endTs && scrumRecord.scrumRecorder) {
isRecording = true
} else {
isRecording = false
}
}
$: scrumRecord &&
txQuery.query(
core.class.TxCUD,
{
modifiedOn: { $gte: scrumRecord.startTs, ...(scrumRecord.endTs ? { $lte: scrumRecord.endTs } : {}) },
modifiedBy: scrumRecord.scrumRecorder
},
(res) => {
txes = res
},
{ sort: { modifiedOn: SortingOrder.Ascending } }
)
</script>
{#if scrumRecord && scrum}
<Panel object={scrumRecord} isUtils={isRecording} isHeader={false} on:close>
<svelte:fragment slot="navigator">
<UpDownNavigator element={scrumRecord} />
<ParentsNavigator element={scrumRecord} />
</svelte:fragment>
<svelte:fragment slot="title">
<span class="fs-title select-text-i">
{scrumRecord.label}
</span>
</svelte:fragment>
<svelte:fragment slot="utils">
{#if isRecording}
<Button
kind="transparent"
showTooltip={{ label: tracker.string.StopRecord }}
icon={tracker.icon.Stop}
on:click={() => scrum && handleRecordingScrum(client, scrum, scrumRecord)}
/>
{/if}
</svelte:fragment>
<svelte:fragment slot="custom-attributes">
<ScrumRecordInfo {scrumRecord} {scrum} />
</svelte:fragment>
<div class="itemsContainer">
<div class="flex-row-center">
<TabList
items={modeList}
selected={mode}
on:select={(result) => {
if (result.detail !== undefined && result.detail.action) result.detail.action()
}}
/>
</div>
</div>
{#if mode === 'timeReports'}
<ScrumRecordTimeSpend {txes} members={scrum.members} {onNavigate} />
{/if}
{#if mode === 'objects'}
<ScrumRecordObjects {txes} {onNavigate} />
{/if}
</Panel>
{/if}
<style lang="scss">
.itemsContainer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 0.75rem 0.65rem 2.25rem;
}
</style>

View File

@ -1,42 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ScrumRecord } from '@hcengineering/tracker'
import { showPanel } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: ScrumRecord
function handleOpenPanel () {
showPanel(tracker.component.ScrumRecordPanel, value._id, value._class, 'content')
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={handleOpenPanel}>
<span title={value.label} class="scrumRecordLabel flex-grow">{value.label}</span>
</div>
<style lang="scss">
.scrumRecordLabel {
display: block;
min-width: 0;
text-align: left;
color: var(--theme-caption-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@ -1,198 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import core, { Doc, Ref, SortingOrder, TxCollectionCUD, TxCreateDoc, TxCUD, TxUpdateDoc } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import { List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
type TimeSpendByEmployee = { [key: Ref<Employee>]: number | undefined }
type TimeSpendByIssue = { [key: Ref<Issue>]: TimeSpendByEmployee | undefined }
type TimeSpendInfo = {
issueId: Ref<Issue>
value: number
employee: Ref<Employee>
}
export let members: Ref<Employee>[]
export let txes: TxCUD<Doc>[] = []
export let onNavigate: () => void
const issuesQuery = createQuery()
let timeSpendInfoByIssue: TimeSpendByIssue = {}
let viewableIssues: Issue[] = []
$: issueTxes = txes.filter((tx) => tx.objectClass === tracker.class.Issue)
$: {
timeSpendInfoByIssue = {}
const timeSpendRecords: { [key: Ref<TimeSpendReport>]: TimeSpendInfo | undefined } = {}
const timeSpendTxes = (issueTxes as TxCollectionCUD<Issue, TimeSpendReport>[]).filter(
(tx) => tx.tx.objectClass === tracker.class.TimeSpendReport
)
const addNewTimeSpend = (
issueId: Ref<Issue>,
timeSpendId: Ref<TimeSpendReport>,
employee?: Ref<Employee> | null,
newValue?: number
) => {
if (newValue && employee && members.includes(employee)) {
if (!(issueId in timeSpendInfoByIssue)) {
timeSpendInfoByIssue[issueId] = {}
}
const recordedValue = timeSpendInfoByIssue[issueId]![employee] ?? 0
timeSpendInfoByIssue[issueId]![employee] = newValue + recordedValue
timeSpendRecords[timeSpendId] = {
issueId,
employee,
value: newValue
}
}
}
timeSpendTxes
.filter((tx) => tx.tx._class === core.class.TxCreateDoc)
.forEach((tx) => {
const timeSpendTxCreate = tx.tx as TxCreateDoc<TimeSpendReport>
const employee = timeSpendTxCreate.attributes.employee
const newValue = timeSpendTxCreate.attributes.value
addNewTimeSpend(tx.objectId, tx.tx.objectId, employee, newValue)
console.log(JSON.stringify({ newValue, employee }))
console.log('TX:', JSON.stringify(tx.tx))
})
timeSpendTxes
.filter((tx) => tx.tx._class === core.class.TxUpdateDoc)
.forEach((tx) => {
const timeSpendTxUpdate = tx.tx as TxUpdateDoc<TimeSpendReport>
const employee = timeSpendTxUpdate.operations.employee
const value = timeSpendTxUpdate.operations.value
const timeSpendId = timeSpendTxUpdate.objectId
const recordedTimeSpend = timeSpendRecords[timeSpendId]
if (employee || value) {
if (recordedTimeSpend) {
const recordedValueByEmployee =
timeSpendInfoByIssue[recordedTimeSpend.issueId]![recordedTimeSpend.employee]!
const newValue = recordedValueByEmployee - recordedTimeSpend.value
if (newValue === 0) {
delete timeSpendInfoByIssue[recordedTimeSpend.issueId]![recordedTimeSpend.employee]
} else {
timeSpendInfoByIssue[recordedTimeSpend.issueId]![recordedTimeSpend.employee] = newValue
}
}
const recordingValue = value ?? recordedTimeSpend?.value
const recordingEmployee = employee ?? recordedTimeSpend?.employee
console.log(JSON.stringify({ recordingValue, recordingEmployee }))
console.log('TX:', JSON.stringify(tx.tx))
addNewTimeSpend(tx.objectId, timeSpendId, recordingEmployee, recordingValue)
}
})
}
// Update reported time and assignee for tracked issues according to tracked TimeSpendReports
$: issuesQuery.query(
tracker.class.Issue,
{
_id: { $in: Object.keys(timeSpendInfoByIssue) as Ref<Issue>[] }
},
(res) => {
const issues = res
viewableIssues = []
for (const [issueId, timeSpendInfo] of Object.entries(timeSpendInfoByIssue)) {
const currentIssue = issues.find((issue) => issue._id === issueId)
if (!timeSpendInfo || !currentIssue) {
return
}
for (const [employeeId, reportedTime] of Object.entries(timeSpendInfo)) {
viewableIssues.push({ ...currentIssue, reportedTime: reportedTime!, assignee: employeeId as Ref<Employee> })
}
viewableIssues = viewableIssues.sort(
(issueLeft, issueRight) => issueRight.reportedTime - issueLeft.reportedTime
)
}
},
{
sort: { priority: SortingOrder.Ascending }
}
)
</script>
<List
_class={tracker.class.Issue}
documents={viewableIssues}
config={[
{
key: '',
presenter: tracker.component.PriorityEditor,
props: { kind: 'list', size: 'small', isEditable: false }
},
{ key: '', presenter: tracker.component.IssuePresenter, props: { onClick: onNavigate } },
{
key: '',
presenter: tracker.component.StatusEditor,
props: { kind: 'list', size: 'small', justify: 'center', isEditable: false }
},
{
key: '',
presenter: tracker.component.TitlePresenter,
props: { shouldUseMargin: true, showParent: false, onClick: onNavigate }
},
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{
key: '',
presenter: tracker.component.DueDatePresenter,
props: { kind: 'list', isEditable: false }
},
{
key: '',
presenter: tracker.component.MilestoneEditor,
displayProps: {
excludeByKey: 'milestone'
},
props: {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
isEditable: false
}
},
{
key: '',
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', isEditable: false }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: {}
}
]}
viewOptions={{ orderBy: ['modifiedOn', SortingOrder.Descending], groupBy: ['assignee'] }}
/>

View File

@ -1,75 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { SortingOrder } from '@hcengineering/core'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { Button, Icon, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
import { List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import RecordScrumButton from './RecordScrumButton.svelte'
import ScrumEditor from './ScrumEditor.svelte'
import ScrumHeader from './ScrumHeader.svelte'
import ScrumRecordPresenter from './ScrumRecordPresenter.svelte'
import { ActionContext } from '@hcengineering/presentation'
export let scrum: Scrum
export let activeScrumRecord: ScrumRecord | undefined
let asideShown = true
$: query = { space: scrum.space, attachedTo: scrum._id }
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<ScrumHeader {scrum}>
<svelte:fragment slot="options">
<RecordScrumButton {scrum} {activeScrumRecord} />
<Button
icon={asideShown ? IconDetailsFilled : IconDetails}
kind={'transparent'}
size={'medium'}
selected={asideShown}
on:click={() => (asideShown = !asideShown)}
/>
</svelte:fragment>
</ScrumHeader>
<div class="top-divider flex w-full h-full clear-mins">
<List
_class={tracker.class.ScrumRecord}
space={scrum.space}
{query}
viewOptions={{
orderBy: ['modifiedOn', SortingOrder.Descending],
groupBy: []
}}
config={[
{ key: '', presenter: Icon, props: { icon: tracker.icon.Scrum, size: 'small' } },
{ key: '', presenter: ScrumRecordPresenter },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter
}
]}
disableHeader
/>
{#if asideShown}
<ScrumEditor bind:scrum />
{/if}
</div>

View File

@ -1,41 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Scrum } from '@hcengineering/tracker'
import { Icon } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: Scrum | undefined
const getMinutes = (date: Date) => {
const currentMinutes = date.getMinutes()
return Math.floor(currentMinutes / 10) > 0 ? currentMinutes : `0${currentMinutes}`
}
</script>
{#if value}
{@const start = new Date(value.beginTime)}
{@const end = new Date(value.endTime)}
<span class="overflow-label flex-row-center flex-grow">
<Icon icon={tracker.icon.Scrum} size={'small'} />
<div class="ml-2 mr-2">
{value.title}
</div>
<span class="flex flex-grow justify-end">
{`${start.getHours()}:${getMinutes(start)}`}
-
{`${end.getHours()}:${getMinutes(end)}`}
</span>
</span>
{/if}

View File

@ -1,70 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Project, Scrum, ScrumRecord } from '@hcengineering/tracker'
import { closePopup, closeTooltip, resolvedLocationStore } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import ScrumRecordsView from './ScrumRecordsView.svelte'
import ScrumsView from './ScrumsView.svelte'
export let currentSpace: Ref<Project>
let scrumId: Ref<Scrum> | undefined
let scrum: Scrum | undefined
let activeScrumRecord: ScrumRecord | undefined
const activeRecordQuery = createQuery()
const scrumQuery = createQuery()
onDestroy(
resolvedLocationStore.subscribe(async (loc) => {
closeTooltip()
closePopup()
scrumId = loc.path[5] as Ref<Scrum>
})
)
$: if (scrumId) {
scrumQuery.query(tracker.class.Scrum, { _id: scrumId }, (res) => {
scrum = res.shift()
})
} else {
scrumQuery.unsubscribe()
scrum = undefined
}
$: activeRecordQuery.query(
tracker.class.ScrumRecord,
{
space: currentSpace,
scrumRecorder: { $exists: true },
startTs: { $exists: true },
endTs: { $exists: false }
},
(result) => {
activeScrumRecord = result.shift()
}
)
</script>
{#if scrum}
<ScrumRecordsView {activeScrumRecord} {scrum} />
{:else}
<ScrumsView {activeScrumRecord} {currentSpace} />
{/if}

View File

@ -1,82 +0,0 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact from '@hcengineering/contact'
import { Ref, SortingOrder } from '@hcengineering/core'
import { ScrumRecord, Project, Scrum } from '@hcengineering/tracker'
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import NewScrum from './NewScrum.svelte'
import RecordScrumPresenter from './RecordScrumPresenter.svelte'
import ScrumDatePresenter from './ScrumDatePresenter.svelte'
import ScrumPresenter from './ScrumPresenter.svelte'
import { ActionContext } from '@hcengineering/presentation'
export let currentSpace: Ref<Project>
export let activeScrumRecord: ScrumRecord | undefined
const showCreateDialog = async () => {
showPopup(NewScrum, { space: currentSpace, targetElement: null }, null)
}
const retrieveMembers = (s: Scrum) => s.members
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<div class="fs-title flex-between header">
<Label label={tracker.string.Scrums} />
<div class="flex-between flex-gap-2">
<Button size="small" icon={IconAdd} label={tracker.string.Scrum} kind={'primary'} on:click={showCreateDialog} />
</div>
</div>
<List
_class={tracker.class.Scrum}
query={{ space: currentSpace }}
space={currentSpace}
config={[
{ key: '', presenter: Icon, props: { icon: tracker.icon.Scrum, size: 'small' } },
{ key: '', presenter: ScrumPresenter, props: { kind: 'list' } },
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ScrumMembersTitle,
intlSearchPh: tracker.string.ScrumMembersSearchPlaceholder,
retrieveMembers
}
},
{ key: '', presenter: ScrumDatePresenter },
{ key: '', presenter: RecordScrumPresenter, props: { activeScrumRecord } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter
}
]}
viewOptions={{ orderBy: ['beginTime', SortingOrder.Ascending], groupBy: [] }}
disableHeader
/>
<style lang="scss">
.header {
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
}
</style>

View File

@ -13,20 +13,10 @@
// limitations under the License.
//
import {
Class,
Client,
Doc,
DocumentQuery,
getCurrentAccount,
Ref,
RelatedDocument,
toIdMap,
TxOperations
} from '@hcengineering/core'
import { Class, Client, Doc, DocumentQuery, Ref, RelatedDocument, toIdMap, TxOperations } from '@hcengineering/core'
import { Resources, translate } from '@hcengineering/platform'
import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
import { Issue, Project, Scrum, ScrumRecord, Milestone } from '@hcengineering/tracker'
import { Issue, Milestone, Project } from '@hcengineering/tracker'
import { showPopup } from '@hcengineering/ui'
import ComponentEditor from './components/components/ComponentEditor.svelte'
import ComponentPresenter from './components/components/ComponentPresenter.svelte'
@ -51,24 +41,24 @@ import KanbanView from './components/issues/KanbanView.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
import NotificationIssuePresenter from './components/issues/NotificationIssuePresenter.svelte'
import PriorityEditor from './components/issues/PriorityEditor.svelte'
import PriorityFilterValuePresenter from './components/issues/PriorityFilterValuePresenter.svelte'
import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
import PriorityRefPresenter from './components/issues/PriorityRefPresenter.svelte'
import RelatedIssueSelector from './components/issues/related/RelatedIssueSelector.svelte'
import RelatedIssuesSection from './components/issues/related/RelatedIssuesSection.svelte'
import StatusEditor from './components/issues/StatusEditor.svelte'
import StatusFilterValuePresenter from './components/issues/StatusFilterValuePresenter.svelte'
import StatusPresenter from './components/issues/StatusPresenter.svelte'
import TitlePresenter from './components/issues/TitlePresenter.svelte'
import PriorityFilterValuePresenter from './components/issues/PriorityFilterValuePresenter.svelte'
import StatusFilterValuePresenter from './components/issues/StatusFilterValuePresenter.svelte'
import ProjectFilterValuePresenter from './components/projects/ProjectFilterValuePresenter.svelte'
import EditMilestone from './components/milestones/EditMilestone.svelte'
import MilestoneDatePresenter from './components/milestones/MilestoneDatePresenter.svelte'
import MyIssues from './components/myissues/MyIssues.svelte'
import NewIssueHeader from './components/NewIssueHeader.svelte'
import NopeComponent from './components/NopeComponent.svelte'
import ProjectFilterValuePresenter from './components/projects/ProjectFilterValuePresenter.svelte'
import RelationsPopup from './components/RelationsPopup.svelte'
import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte'
import MilestoneDatePresenter from './components/milestones/MilestoneDatePresenter.svelte'
import EditMilestone from './components/milestones/EditMilestone.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import Statuses from './components/workflow/Statuses.svelte'
@ -87,13 +77,10 @@ import MilestoneEditor from './components/milestones/MilestoneEditor.svelte'
import MilestonePresenter from './components/milestones/MilestonePresenter.svelte'
import Milestones from './components/milestones/Milestones.svelte'
import MilestoneSelector from './components/milestones/MilestoneSelector.svelte'
import MilestoneStatusPresenter from './components/milestones/MilestoneStatusPresenter.svelte'
import MilestoneStatusEditor from './components/milestones/MilestoneStatusEditor.svelte'
import MilestoneStatusPresenter from './components/milestones/MilestoneStatusPresenter.svelte'
import MilestoneTitlePresenter from './components/milestones/MilestoneTitlePresenter.svelte'
import ScrumRecordPanel from './components/scrums/ScrumRecordPanel.svelte'
import Scrums from './components/scrums/Scrums.svelte'
import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte'
@ -113,17 +100,17 @@ import EditIssueTemplate from './components/templates/EditIssueTemplate.svelte'
import TemplateEstimationEditor from './components/templates/EstimationEditor.svelte'
import {
getAllComponents,
getAllPriority,
getAllMilestones,
getAllPriority,
getVisibleFilters,
issuePrioritySort,
issueStatusSort,
moveIssuesToAnotherMilestone,
milestoneSort,
subIssueQuery,
getVisibleFilters
moveIssuesToAnotherMilestone,
subIssueQuery
} from './utils'
import { EmployeeAccount } from '@hcengineering/contact'
import { ComponentAggregationManager, grouppingComponentManager } from './component'
import PriorityIcon from './components/activity/PriorityIcon.svelte'
import StatusIcon from './components/activity/StatusIcon.svelte'
import TxIssueCreated from './components/activity/TxIssueCreated.svelte'
@ -131,13 +118,12 @@ import DeleteComponentPresenter from './components/components/DeleteComponentPre
import MoveIssues from './components/issues/Move.svelte'
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
import TimeSpendReportPopup from './components/issues/timereport/TimeSpendReportPopup.svelte'
import IssueStatistics from './components/milestones/IssueStatistics.svelte'
import MilestoneFilter from './components/milestones/MilestoneFilter.svelte'
import MilestoneRefPresenter from './components/milestones/MilestoneRefPresenter.svelte'
import CreateProject from './components/projects/CreateProject.svelte'
import ProjectPresenter from './components/projects/ProjectPresenter.svelte'
import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.svelte'
import IssueStatistics from './components/milestones/IssueStatistics.svelte'
import MilestoneRefPresenter from './components/milestones/MilestoneRefPresenter.svelte'
import MilestoneFilter from './components/milestones/MilestoneFilter.svelte'
import { ComponentAggregationManager, grouppingComponentManager } from './component'
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
@ -338,79 +324,6 @@ async function deleteMilestone (milestones: Milestone | Milestone[]): Promise<vo
}
}
async function startRecordingScrum (
client: TxOperations,
newRecordingScrum: Scrum,
previousScrumRecord?: ScrumRecord
): Promise<void> {
const newRecordLabel = `${newRecordingScrum.title}-${newRecordingScrum.scrumRecords ?? 0}`
const startRecord = async (): Promise<void> => {
await client.addCollection(
tracker.class.ScrumRecord,
newRecordingScrum.space,
newRecordingScrum._id,
tracker.class.Scrum,
'scrumRecords',
{
label: newRecordLabel,
scrumRecorder: getCurrentAccount()._id as Ref<EmployeeAccount>,
startTs: Date.now(),
comments: 0
}
)
}
if (previousScrumRecord !== undefined) {
showPopup(
MessageBox,
{
label: tracker.string.ChangeScrumRecord,
message: tracker.string.ChangeScrumRecordConfirm,
params: { previousRecord: previousScrumRecord.label, newRecord: newRecordLabel }
},
undefined,
(result?: boolean) => {
if (result === true) {
void client
.updateCollection(
tracker.class.ScrumRecord,
previousScrumRecord.space,
previousScrumRecord._id,
previousScrumRecord.attachedTo,
tracker.class.Scrum,
'scrumRecords',
{ endTs: Date.now() }
)
.then(async () => await startRecord())
}
}
)
} else {
await startRecord()
}
}
export async function handleRecordingScrum (
client: TxOperations,
currentScrum: Scrum,
activeScrumRecord?: ScrumRecord
): Promise<void> {
// Stop recording scrum if active record attached to current scrum
if (activeScrumRecord?.attachedTo === currentScrum._id) {
await client.updateCollection(
tracker.class.ScrumRecord,
activeScrumRecord.space,
activeScrumRecord._id,
activeScrumRecord.attachedTo,
tracker.class.Scrum,
'scrumRecords',
{ endTs: Date.now() }
)
} else {
await startRecordingScrum(client, currentScrum, activeScrumRecord)
}
}
export default async (): Promise<Resources> => ({
activity: {
TxIssueCreated,
@ -455,8 +368,6 @@ export default async (): Promise<Resources> => ({
Milestones,
MilestonePresenter,
EditMilestone,
Scrums,
ScrumRecordPanel,
MilestoneStatusPresenter,
MilestoneStatusEditor,
MilestoneTitlePresenter,

View File

@ -249,26 +249,6 @@ export default mergeIds(trackerId, tracker, {
MoveAndDeleteMilestone: '' as IntlString,
MoveAndDeleteMilestoneConfirm: '' as IntlString,
Scrum: '' as IntlString,
Scrums: '' as IntlString,
ScrumMembersTitle: '' as IntlString,
ScrumMembersSearchPlaceholder: '' as IntlString,
ScrumBeginTime: '' as IntlString,
ScrumEndTime: '' as IntlString,
NewScrum: '' as IntlString,
CreateScrum: '' as IntlString,
ScrumTitlePlaceholder: '' as IntlString,
ScrumDescriptionPlaceholder: '' as IntlString,
ScrumRecords: '' as IntlString,
ScrumRecord: '' as IntlString,
StartRecord: '' as IntlString,
StopRecord: '' as IntlString,
ChangeScrumRecord: '' as IntlString,
ChangeScrumRecordConfirm: '' as IntlString,
ScrumRecorder: '' as IntlString,
ScrumRecordTimeReports: '' as IntlString,
ScrumRecordObjects: '' as IntlString,
Estimation: '' as IntlString,
ReportedTime: '' as IntlString,
TimeSpendReport: '' as IntlString,
@ -371,9 +351,6 @@ export default mergeIds(trackerId, tracker, {
TemplateEstimationEditor: '' as AnyComponent,
DeleteComponentPresenter: '' as AnyComponent,
Scrums: '' as AnyComponent,
ScrumRecordPanel: '' as AnyComponent,
ComponentSelector: '' as AnyComponent,
IssueTemplates: '' as AnyComponent,

View File

@ -228,8 +228,6 @@ export type ComponentsFilterMode = 'all' | 'backlog' | 'active' | 'closed'
export type MilestoneViewMode = 'all' | 'planned' | 'active' | 'closed'
export type ScrumRecordViewMode = 'timeReports' | 'objects'
export const getIncludedMilestoneStatuses = (mode: MilestoneViewMode): MilestoneStatus[] => {
switch (mode) {
case 'all': {
@ -264,11 +262,6 @@ export const milestoneTitleMap: Record<MilestoneViewMode, IntlString> = Object.f
closed: tracker.string.ClosedMilestones
})
export const scrumRecordTitleMap: Record<ScrumRecordViewMode, IntlString> = Object.freeze({
timeReports: tracker.string.ScrumRecordTimeReports,
objects: tracker.string.ScrumRecordObjects
})
const listIssueStatusOrder = [
tracker.issueStatusCategory.Started,
tracker.issueStatusCategory.Unstarted,

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { Employee } from '@hcengineering/contact'
import {
AttachedDoc,
Attribute,
@ -47,7 +47,6 @@ export interface IssueStatus extends Status {}
export interface Project extends Space, IconProps {
identifier: string // Project identifier
sequence: number
issueStatuses: number
defaultIssueStatus: Ref<IssueStatus>
defaultAssignee?: Ref<Employee>
defaultTimeReportDay: TimeReportDayType
@ -332,37 +331,6 @@ export class ComponentManager extends DocManager {
}
}
/**
* @public
*/
export interface ScrumRecord extends AttachedDoc {
label: string
startTs: Timestamp
endTs?: Timestamp
scrumRecorder: Ref<EmployeeAccount>
comments: number
attachments?: number
space: Ref<Project>
attachedTo: Ref<Scrum>
}
/**
* @public
*/
export interface Scrum extends Doc {
title: string
description?: Markup
beginTime: Timestamp
endTime: Timestamp
members: Ref<Employee>[]
space: Ref<Project>
scrumRecords?: number
attachments?: number
}
/**
* @public
*/
@ -379,8 +347,6 @@ export default plugin(trackerId, {
IssueStatus: '' as Ref<Class<IssueStatus>>,
TypeIssuePriority: '' as Ref<Class<Type<IssuePriority>>>,
Milestone: '' as Ref<Class<Milestone>>,
Scrum: '' as Ref<Class<Scrum>>,
ScrumRecord: '' as Ref<Class<ScrumRecord>>,
TypeMilestoneStatus: '' as Ref<Class<Type<MilestoneStatus>>>,
TimeSpendReport: '' as Ref<Class<TimeSpendReport>>,
TypeReportedTime: '' as Ref<Class<Type<number>>>
@ -430,7 +396,6 @@ export default plugin(trackerId, {
Parent: '' as Asset,
Milestone: '' as Asset,
IssueTemplates: '' as Asset,
Scrum: '' as Asset,
Start: '' as Asset,
Stop: '' as Asset,

View File

@ -187,11 +187,8 @@ class ElasticDataAdapter implements DbAdapter {
},
undefined
)
} catch (e: any) {
if (e?.meta?.body?.error?.type !== 'index_not_found_exception') {
console.error(e)
throw new PlatformError(e)
}
} catch (err: any) {
console.error(err)
}
const operations = part.flatMap((doc) => [