mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-04 06:18:34 +00:00
UBER-972 (#3782)
This commit is contained in:
parent
f1c4726749
commit
9a4fa752de
@ -29,6 +29,7 @@ import {
|
|||||||
Domain,
|
Domain,
|
||||||
DOMAIN_BLOB,
|
DOMAIN_BLOB,
|
||||||
DOMAIN_CONFIGURATION,
|
DOMAIN_CONFIGURATION,
|
||||||
|
DOMAIN_MIGRATION,
|
||||||
DOMAIN_DOC_INDEX_STATE,
|
DOMAIN_DOC_INDEX_STATE,
|
||||||
DOMAIN_FULLTEXT_BLOB,
|
DOMAIN_FULLTEXT_BLOB,
|
||||||
DOMAIN_MODEL,
|
DOMAIN_MODEL,
|
||||||
@ -49,7 +50,8 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
Type,
|
Type,
|
||||||
Version
|
Version,
|
||||||
|
MigrationState
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
Hidden,
|
Hidden,
|
||||||
@ -244,6 +246,12 @@ export class TVersion extends TDoc implements Version {
|
|||||||
patch!: number
|
patch!: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Model(core.class.MigrationState, core.class.Doc, DOMAIN_MIGRATION)
|
||||||
|
export class TMigrationState extends TDoc implements MigrationState {
|
||||||
|
plugin!: string
|
||||||
|
state!: string
|
||||||
|
}
|
||||||
|
|
||||||
@Model(core.class.PluginConfiguration, core.class.Doc, DOMAIN_MODEL)
|
@Model(core.class.PluginConfiguration, core.class.Doc, DOMAIN_MODEL)
|
||||||
export class TPluginConfiguration extends TDoc implements PluginConfiguration {
|
export class TPluginConfiguration extends TDoc implements PluginConfiguration {
|
||||||
pluginId!: Plugin
|
pluginId!: Plugin
|
||||||
|
@ -13,11 +13,27 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import core, { DOMAIN_STATUS, DOMAIN_TX, Status, TxCUD, TxOperations, TxProcessor } from '@hcengineering/core'
|
import core, {
|
||||||
import { MigrateOperation, MigrationClient, MigrationUpgradeClient, createOrUpdate } from '@hcengineering/model'
|
DOMAIN_STATUS,
|
||||||
|
DOMAIN_TX,
|
||||||
|
Status,
|
||||||
|
TxCUD,
|
||||||
|
TxCollectionCUD,
|
||||||
|
TxCreateDoc,
|
||||||
|
TxOperations,
|
||||||
|
TxProcessor,
|
||||||
|
TxUpdateDoc
|
||||||
|
} from '@hcengineering/core'
|
||||||
|
import {
|
||||||
|
MigrateOperation,
|
||||||
|
MigrationClient,
|
||||||
|
MigrationUpgradeClient,
|
||||||
|
createOrUpdate,
|
||||||
|
tryMigrate
|
||||||
|
} from '@hcengineering/model'
|
||||||
import { DOMAIN_TASK } from '@hcengineering/model-task'
|
import { DOMAIN_TASK } from '@hcengineering/model-task'
|
||||||
import tags from '@hcengineering/tags'
|
import tags from '@hcengineering/tags'
|
||||||
import { Project, TimeReportDayType, createStatuses } from '@hcengineering/tracker'
|
import { Issue, Project, TimeReportDayType, TimeSpendReport, createStatuses } from '@hcengineering/tracker'
|
||||||
import { DOMAIN_TRACKER } from '.'
|
import { DOMAIN_TRACKER } from '.'
|
||||||
import tracker from './plugin'
|
import tracker from './plugin'
|
||||||
import { DOMAIN_SPACE } from '@hcengineering/model-core'
|
import { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||||
@ -83,6 +99,44 @@ async function fixIconsWithEmojis (tx: TxOperations): Promise<void> {
|
|||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fixSpentTime (client: MigrationClient): Promise<void> {
|
||||||
|
const issues = await client.find<Issue>(DOMAIN_TASK, { reportedTime: { $gt: 0 } })
|
||||||
|
for (const issue of issues) {
|
||||||
|
const childInfo = issue.childInfo
|
||||||
|
for (const child of childInfo) {
|
||||||
|
child.reportedTime = child.reportedTime * 8
|
||||||
|
}
|
||||||
|
await client.update(DOMAIN_TASK, { _id: issue._id }, { reportedTime: issue.reportedTime * 8, childInfo })
|
||||||
|
}
|
||||||
|
const reports = await client.find<TimeSpendReport>(DOMAIN_TRACKER, {})
|
||||||
|
for (const report of reports) {
|
||||||
|
await client.update(DOMAIN_TRACKER, { _id: report._id }, { value: report.value * 8 })
|
||||||
|
}
|
||||||
|
const createTxes = await client.find<TxCollectionCUD<Issue, TimeSpendReport>>(DOMAIN_TX, {
|
||||||
|
'tx.objectClass': tracker.class.TimeSpendReport,
|
||||||
|
'tx._class': core.class.TxCreateDoc,
|
||||||
|
'tx.attributes.value': { $exists: true }
|
||||||
|
})
|
||||||
|
for (const tx of createTxes) {
|
||||||
|
await client.update(
|
||||||
|
DOMAIN_TX,
|
||||||
|
{ _id: tx._id },
|
||||||
|
{ 'tx.attributes.value': (tx.tx as TxCreateDoc<TimeSpendReport>).attributes.value * 8 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const updateTxes = await client.find<TxCollectionCUD<Issue, TimeSpendReport>>(DOMAIN_TX, {
|
||||||
|
'tx.objectClass': tracker.class.TimeSpendReport,
|
||||||
|
'tx._class': core.class.TxUpdateDoc,
|
||||||
|
'tx.operations.value': { $exists: true }
|
||||||
|
})
|
||||||
|
for (const tx of updateTxes) {
|
||||||
|
const val = (tx.tx as TxUpdateDoc<TimeSpendReport>).operations.value
|
||||||
|
if (val !== undefined) {
|
||||||
|
await client.update(DOMAIN_TX, { _id: tx._id }, { 'tx.operations.value': val * 8 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function moveIssues (client: MigrationClient): Promise<void> {
|
async function moveIssues (client: MigrationClient): Promise<void> {
|
||||||
const docs = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue })
|
const docs = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue })
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
@ -114,8 +168,20 @@ async function fixProjectDefaultStatuses (client: MigrationClient): Promise<void
|
|||||||
|
|
||||||
export const trackerOperation: MigrateOperation = {
|
export const trackerOperation: MigrateOperation = {
|
||||||
async migrate (client: MigrationClient): Promise<void> {
|
async migrate (client: MigrationClient): Promise<void> {
|
||||||
await moveIssues(client)
|
await tryMigrate(client, 'tracker', [
|
||||||
await fixProjectDefaultStatuses(client)
|
{
|
||||||
|
state: 'moveIssues',
|
||||||
|
func: moveIssues
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'fixProjectDefaultStatuses',
|
||||||
|
func: fixProjectDefaultStatuses
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'reportTimeDayToHour',
|
||||||
|
func: fixSpentTime
|
||||||
|
}
|
||||||
|
])
|
||||||
},
|
},
|
||||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||||
const tx = new TxOperations(client, core.account.System)
|
const tx = new TxOperations(client, core.account.System)
|
||||||
|
@ -289,6 +289,11 @@ export const DOMAIN_MODEL = 'model' as Domain
|
|||||||
*/
|
*/
|
||||||
export const DOMAIN_CONFIGURATION = '_configuration' as Domain
|
export const DOMAIN_CONFIGURATION = '_configuration' as Domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const DOMAIN_MIGRATION = '_migrations' as Domain
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -358,6 +363,14 @@ export interface Version extends Doc {
|
|||||||
patch: number
|
patch: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface MigrationState extends Doc {
|
||||||
|
plugin: string
|
||||||
|
state: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -35,6 +35,7 @@ import type {
|
|||||||
IndexStageState,
|
IndexStageState,
|
||||||
IndexingConfiguration,
|
IndexingConfiguration,
|
||||||
Interface,
|
Interface,
|
||||||
|
MigrationState,
|
||||||
Obj,
|
Obj,
|
||||||
PluginConfiguration,
|
PluginConfiguration,
|
||||||
Ref,
|
Ref,
|
||||||
@ -118,7 +119,8 @@ export default plugin(coreId, {
|
|||||||
Configuration: '' as Ref<Class<Configuration>>,
|
Configuration: '' as Ref<Class<Configuration>>,
|
||||||
|
|
||||||
Status: '' as Ref<Class<Status>>,
|
Status: '' as Ref<Class<Status>>,
|
||||||
StatusCategory: '' as Ref<Class<StatusCategory>>
|
StatusCategory: '' as Ref<Class<StatusCategory>>,
|
||||||
|
MigrationState: '' as Ref<Class<MigrationState>>
|
||||||
},
|
},
|
||||||
mixin: {
|
mixin: {
|
||||||
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>,
|
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import core, {
|
||||||
Client,
|
Client,
|
||||||
Doc,
|
Doc,
|
||||||
|
DOMAIN_MIGRATION,
|
||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
Domain,
|
Domain,
|
||||||
FindOptions,
|
FindOptions,
|
||||||
@ -10,7 +11,11 @@ import {
|
|||||||
ObjQueryType,
|
ObjQueryType,
|
||||||
PushOptions,
|
PushOptions,
|
||||||
Ref,
|
Ref,
|
||||||
UnsetOptions
|
UnsetOptions,
|
||||||
|
MigrationState,
|
||||||
|
generateId,
|
||||||
|
TxOperations,
|
||||||
|
Data
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,3 +99,65 @@ export interface MigrateOperation {
|
|||||||
// Perform high level upgrade operations.
|
// Perform high level upgrade operations.
|
||||||
upgrade: (client: MigrationUpgradeClient) => Promise<void>
|
upgrade: (client: MigrationUpgradeClient) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface Migrations {
|
||||||
|
state: string
|
||||||
|
func: (client: MigrationClient) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface UpgradeOperations {
|
||||||
|
state: string
|
||||||
|
func: (client: MigrationUpgradeClient) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function tryMigrate (client: MigrationClient, plugin: string, migrations: Migrations[]): Promise<void> {
|
||||||
|
const states = new Set(
|
||||||
|
(await client.find<MigrationState>(DOMAIN_MIGRATION, { _class: core.class.MigrationState, plugin })).map(
|
||||||
|
(p) => p.state
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for (const migration of migrations) {
|
||||||
|
if (states.has(migration.state)) continue
|
||||||
|
await migration.func(client)
|
||||||
|
const st: MigrationState = {
|
||||||
|
plugin,
|
||||||
|
state: migration.state,
|
||||||
|
space: core.space.Configuration,
|
||||||
|
modifiedBy: core.account.System,
|
||||||
|
modifiedOn: Date.now(),
|
||||||
|
_class: core.class.MigrationState,
|
||||||
|
_id: generateId()
|
||||||
|
}
|
||||||
|
await client.create(DOMAIN_MIGRATION, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function tryUpgrade (
|
||||||
|
client: MigrationUpgradeClient,
|
||||||
|
plugin: string,
|
||||||
|
migrations: UpgradeOperations[]
|
||||||
|
): Promise<void> {
|
||||||
|
const states = new Set((await client.findAll(core.class.MigrationState, { plugin })).map((p) => p.state))
|
||||||
|
for (const migration of migrations) {
|
||||||
|
if (states.has(migration.state)) continue
|
||||||
|
await migration.func(client)
|
||||||
|
const st: Data<MigrationState> = {
|
||||||
|
plugin,
|
||||||
|
state: migration.state
|
||||||
|
}
|
||||||
|
const tx = new TxOperations(client, core.account.System)
|
||||||
|
await tx.createDoc(core.class.MigrationState, core.space.Configuration, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -29,15 +29,15 @@
|
|||||||
on:click
|
on:click
|
||||||
use:tooltip={{
|
use:tooltip={{
|
||||||
component: Label,
|
component: Label,
|
||||||
props: { label: tracker.string.TimeSpendHours, params: { value: floorFractionDigits(value * 8, 3) } }
|
props: { label: tracker.string.TimeSpendHours, params: { value: floorFractionDigits(value, 1) } }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if noSymbol}
|
{#if noSymbol}
|
||||||
{floorFractionDigits(value, 1)}
|
{floorFractionDigits(value, 1)}
|
||||||
{:else if value > 0 && value < 1}
|
{:else if value > 0 && value < 8}
|
||||||
<Label label={tracker.string.TimeSpendHours} params={{ value: floorFractionDigits(value * 8, 1) }} />
|
<Label label={tracker.string.TimeSpendHours} params={{ value: floorFractionDigits(value, 1) }} />
|
||||||
{:else}
|
{:else}
|
||||||
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value, 1) }} />
|
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value / 8, 3) }} />
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -109,22 +109,22 @@
|
|||||||
maxDigitsAfterPoint={3}
|
maxDigitsAfterPoint={3}
|
||||||
kind={'editbox'}
|
kind={'editbox'}
|
||||||
/>
|
/>
|
||||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.125)}
|
<Button kind={'link-bordered'} on:click={() => (data.value = 1)}
|
||||||
><span slot="content">1<Label label={tracker.string.HourLabel} /></span></Button
|
><span slot="content">1<Label label={tracker.string.HourLabel} /></span></Button
|
||||||
>
|
>
|
||||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.25)}
|
<Button kind={'link-bordered'} on:click={() => (data.value = 2)}
|
||||||
><span slot="content">2<Label label={tracker.string.HourLabel} /></span></Button
|
><span slot="content">2<Label label={tracker.string.HourLabel} /></span></Button
|
||||||
>
|
>
|
||||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.5)}
|
<Button kind={'link-bordered'} on:click={() => (data.value = 4)}
|
||||||
><span slot="content">4<Label label={tracker.string.HourLabel} /></span></Button
|
><span slot="content">4<Label label={tracker.string.HourLabel} /></span></Button
|
||||||
>
|
>
|
||||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.75)}
|
<Button kind={'link-bordered'} on:click={() => (data.value = 6)}
|
||||||
><span slot="content">6<Label label={tracker.string.HourLabel} /></span></Button
|
><span slot="content">6<Label label={tracker.string.HourLabel} /></span></Button
|
||||||
>
|
>
|
||||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.875)}
|
<Button kind={'link-bordered'} on:click={() => (data.value = 7)}
|
||||||
><span slot="content">7<Label label={tracker.string.HourLabel} /></span></Button
|
><span slot="content">7<Label label={tracker.string.HourLabel} /></span></Button
|
||||||
>
|
>
|
||||||
<Button kind={'link-bordered'} on:click={() => (data.value = 1)}
|
<Button kind={'link-bordered'} on:click={() => (data.value = 8)}
|
||||||
><span slot="content">8<Label label={tracker.string.HourLabel} /></span></Button
|
><span slot="content">8<Label label={tracker.string.HourLabel} /></span></Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<Label label={tracker.string.ReportedTime} />:
|
<Label label={tracker.string.ReportedTime} />:
|
||||||
<span class="caption-color"><TimePresenter value={reportedTime} /></span>.
|
<span class="caption-color"><TimePresenter value={reportedTime} /></span>.
|
||||||
<Label label={tracker.string.TimeSpendReports} />:
|
<Label label={tracker.string.TimeSpendReports} />:
|
||||||
<span class="caption-color"><TimePresenter value={total} /></span>
|
<span class="caption-color"><TimePresenter value={floorFractionDigits(total / 8, 3)} /></span>
|
||||||
</span>
|
</span>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<TimeSpendReportsList {reports} {projects} />
|
<TimeSpendReportsList {reports} {projects} />
|
||||||
|
@ -281,7 +281,7 @@ export interface TimeSpendReport extends AttachedDoc {
|
|||||||
employee: Ref<Employee> | null
|
employee: Ref<Employee> | null
|
||||||
|
|
||||||
date: Timestamp | null
|
date: Timestamp | null
|
||||||
// Value in man days
|
// Value in man hours
|
||||||
value: number
|
value: number
|
||||||
|
|
||||||
description: string
|
description: string
|
||||||
|
@ -304,6 +304,7 @@
|
|||||||
}
|
}
|
||||||
if (app === undefined && !navigateDone) {
|
if (app === undefined && !navigateDone) {
|
||||||
const appShort = getMetadata(workbench.metadata.DefaultApplication) as Ref<Application>
|
const appShort = getMetadata(workbench.metadata.DefaultApplication) as Ref<Application>
|
||||||
|
if (appShort == null) return
|
||||||
const spaceRef = getMetadata(workbench.metadata.DefaultSpace) as Ref<Space>
|
const spaceRef = getMetadata(workbench.metadata.DefaultSpace) as Ref<Space>
|
||||||
const specialRef = getMetadata(workbench.metadata.DefaultSpecial) as Ref<Space>
|
const specialRef = getMetadata(workbench.metadata.DefaultSpecial) as Ref<Space>
|
||||||
const loc = getCurrentLocation()
|
const loc = getCurrentLocation()
|
||||||
|
@ -534,7 +534,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
|
|
||||||
async checkIndexConsistency (dbStorage: ServerStorage): Promise<void> {
|
async checkIndexConsistency (dbStorage: ServerStorage): Promise<void> {
|
||||||
if (process.env.MODEL_VERSION !== undefined) {
|
if (process.env.MODEL_VERSION !== undefined) {
|
||||||
const modelVersion = (await this.model.findAll(core.class.Version, {})).shift()
|
const modelVersion = (await this.model.findAll(core.class.Version, {}))[0]
|
||||||
if (modelVersion !== undefined) {
|
if (modelVersion !== undefined) {
|
||||||
const modelVersionString = versionToString(modelVersion)
|
const modelVersionString = versionToString(modelVersion)
|
||||||
if (modelVersionString !== process.env.MODEL_VERSION) {
|
if (modelVersionString !== process.env.MODEL_VERSION) {
|
||||||
|
@ -132,10 +132,10 @@ function floorFractionDigits (n: number | string, amount: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toTime (value: number): string {
|
function toTime (value: number): string {
|
||||||
if (value > 0 && value < 1) {
|
if (value > 0 && value < 8) {
|
||||||
return `${floorFractionDigits(value * 8, 1)}h`
|
return `${floorFractionDigits(value, 1)}h`
|
||||||
} else {
|
} else {
|
||||||
return `${floorFractionDigits(value, 1)}d`
|
return `${floorFractionDigits(value / 8, 3)}d`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ test('report-time-from-issue-card', async ({ page }) => {
|
|||||||
await navigate(page)
|
await navigate(page)
|
||||||
const assignee = 'Chen Rosamund'
|
const assignee = 'Chen Rosamund'
|
||||||
const status = 'In Progress'
|
const status = 'In Progress'
|
||||||
const values = [0.25, 0.5, 0.75, 1]
|
const values = [2, 4, 6, 8]
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const random = Math.floor(Math.random() * values.length)
|
const random = Math.floor(Math.random() * values.length)
|
||||||
const time = values[random]
|
const time = values[random]
|
||||||
@ -214,7 +214,7 @@ test('report-time-from-main-view', async ({ page }) => {
|
|||||||
await page.click('text="Modified date"')
|
await page.click('text="Modified date"')
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
|
|
||||||
const values = [0.25, 0.5, 0.75, 1]
|
const values = [2, 4, 6, 8]
|
||||||
const assignee = 'Chen Rosamund'
|
const assignee = 'Chen Rosamund'
|
||||||
const status = 'In Progress'
|
const status = 'In Progress'
|
||||||
const name = getIssueName()
|
const name = getIssueName()
|
||||||
|
Loading…
Reference in New Issue
Block a user