mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
UBER-430: Remove old migrations (#3398)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
ed964ed362
commit
e809a67451
@ -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)
|
||||
|
@ -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> {}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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, {
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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> {}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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": "Отчеты по времени",
|
||||
|
@ -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`,
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}
|
@ -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>
|
@ -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)}
|
||||
/>
|
@ -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)}
|
||||
/>
|
@ -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
|
||||
/>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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
|
||||
/>
|
@ -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>
|
@ -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>
|
@ -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'] }}
|
||||
/>
|
@ -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>
|
@ -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}
|
@ -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}
|
@ -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>
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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) => [
|
||||
|
Loading…
Reference in New Issue
Block a user