UBERF-4786 (#4285)

This commit is contained in:
Denis Bykhov 2023-12-29 13:05:27 +06:00 committed by GitHub
parent d35cd21f06
commit d6291c7d70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 176 additions and 86 deletions

View File

@ -102,6 +102,7 @@ async function genVacansyApplicants (
members: [], members: [],
archived: false, archived: false,
tasks: [], tasks: [],
classic: true,
// TODO: Fix me. // TODO: Fix me.
statuses: states.map((s) => { statuses: states.map((s) => {
return { _id: s, taskType: '' as Ref<TaskType> } return { _id: s, taskType: '' as Ref<TaskType> }

View File

@ -58,7 +58,8 @@ async function createDefaultProjectType (tx: TxOperations): Promise<Ref<ProjectT
name: 'Default board', name: 'Default board',
descriptor: board.descriptors.BoardType, descriptor: board.descriptors.BoardType,
description: '', description: '',
tasks: [] tasks: [],
classic: true
}, },
[ [
{ {

View File

@ -38,7 +38,8 @@ async function createSpace (tx: TxOperations): Promise<void> {
name: 'Default funnel', name: 'Default funnel',
descriptor: lead.descriptors.FunnelType, descriptor: lead.descriptors.FunnelType,
description: '', description: '',
tasks: [] tasks: [],
classic: true
}, },
[ [
{ {

View File

@ -136,7 +136,8 @@ async function createDefaultKanbanTemplate (tx: TxOperations): Promise<Ref<Proje
name: 'Default vacancy', name: 'Default vacancy',
descriptor: recruit.descriptors.VacancyType, descriptor: recruit.descriptors.VacancyType,
description: '', description: '',
tasks: [] tasks: [],
classic: true
}, },
[ [
{ {

View File

@ -49,7 +49,8 @@ import {
TypeString, TypeString,
UX, UX,
type Builder, type Builder,
type MigrationClient type MigrationClient,
ReadOnly
} from '@hcengineering/model' } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment' import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter' import chunter from '@hcengineering/model-chunter'
@ -106,6 +107,7 @@ export class TTask extends TAttachedDoc implements Task {
@Prop(TypeRef(task.class.TaskType), task.string.TaskType) @Prop(TypeRef(task.class.TaskType), task.string.TaskType)
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
@ReadOnly()
kind!: Ref<TaskType> kind!: Ref<TaskType>
@Prop(TypeString(), task.string.TaskNumber) @Prop(TypeString(), task.string.TaskNumber)
@ -203,6 +205,9 @@ export class TProjectType extends TSpace implements ProjectType {
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class')) @Prop(TypeRef(core.class.Class), getEmbeddedLabel('Target Class'))
targetClass!: Ref<Class<Project>> targetClass!: Ref<Class<Project>>
@Prop(TypeBoolean(), getEmbeddedLabel('Classic'))
classic!: boolean
} }
@Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK) @Model(task.class.TaskType, core.class.Doc, DOMAIN_TASK)
@ -506,7 +511,7 @@ export function createModel (builder: Builder): void {
icon: task.icon.TaskState, icon: task.icon.TaskState,
color: PaletteColorIndexes.Porpoise, color: PaletteColorIndexes.Porpoise,
defaultStatusName: 'New state', defaultStatusName: 'New state',
order: 0 order: 1
}, },
task.statusCategory.Active task.statusCategory.Active
) )
@ -520,7 +525,7 @@ export function createModel (builder: Builder): void {
icon: task.icon.TaskState, icon: task.icon.TaskState,
color: PaletteColorIndexes.Grass, color: PaletteColorIndexes.Grass,
defaultStatusName: 'Won', defaultStatusName: 'Won',
order: 0 order: 2
}, },
task.statusCategory.Won task.statusCategory.Won
) )
@ -534,7 +539,7 @@ export function createModel (builder: Builder): void {
icon: task.icon.TaskState, icon: task.icon.TaskState,
color: PaletteColorIndexes.Coin, color: PaletteColorIndexes.Coin,
defaultStatusName: 'Lost', defaultStatusName: 'Lost',
order: 0 order: 3
}, },
task.statusCategory.Lost task.statusCategory.Lost
) )

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { TxOperations, type Class, type Doc, type Ref } from '@hcengineering/core' import { TxOperations, type Class, type Doc, type Ref, toIdMap } from '@hcengineering/core'
import { import {
createOrUpdate, createOrUpdate,
tryMigrate, tryMigrate,
@ -39,6 +39,26 @@ export async function createSequence (tx: TxOperations, _class: Ref<Class<Doc>>)
} }
} }
async function reorderStates (_client: MigrationUpgradeClient): Promise<void> {
const client = new TxOperations(_client, core.account.System)
const states = toIdMap(await client.findAll(core.class.Status, {}))
const order = [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
]
const taskTypes = await client.findAll(task.class.TaskType, {})
for (const taskType of taskTypes) {
const statuses = [...taskType.statuses].sort((a, b) => {
const aIndex = order.indexOf(states.get(a)?.category ?? task.statusCategory.UnStarted)
const bIndex = order.indexOf(states.get(b)?.category ?? task.statusCategory.UnStarted)
return aIndex - bIndex
})
await client.diffUpdate(taskType, { statuses })
}
}
async function createDefaultSequence (tx: TxOperations): Promise<void> { async function createDefaultSequence (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, { const current = await tx.findOne(core.class.Space, {
_id: task.space.Sequence _id: task.space.Sequence
@ -92,6 +112,18 @@ export const taskOperation: MigrateOperation = {
func: async (client) => { func: async (client) => {
await client.update(DOMAIN_SPACE, { space: core.space.Model }, { space: core.space.Space }) await client.update(DOMAIN_SPACE, { space: core.space.Model }, { space: core.space.Space })
} }
},
{
state: 'classicProjectTypes',
func: async (client) => {
await client.update(
DOMAIN_SPACE,
{ _class: task.class.ProjectType, classic: { $exists: false } },
{
classic: true
}
)
}
} }
]) ])
}, },
@ -113,6 +145,11 @@ export const taskOperation: MigrateOperation = {
task.category.TaskTag task.category.TaskTag
) )
await tryUpgrade(client, taskId, []) await tryUpgrade(client, taskId, [
{
state: 'reorderStates',
func: reorderStates
}
])
} }
} }

View File

@ -58,7 +58,8 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
name: 'Classic project', name: 'Classic project',
descriptor: tracker.descriptors.ProjectType, descriptor: tracker.descriptors.ProjectType,
description: '', description: '',
tasks: [] tasks: [],
classic: true
}, },
[ [
{ {
@ -88,7 +89,8 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
name: 'Base project', name: 'Base project',
descriptor: tracker.descriptors.ProjectType, descriptor: tracker.descriptors.ProjectType,
description: '', description: '',
tasks: [] tasks: [],
classic: false
}, },
[ [
{ {

View File

@ -81,6 +81,8 @@
"StatusChange": "Status changed", "StatusChange": "Status changed",
"TaskCreated": "Task created", "TaskCreated": "Task created",
"TaskType": "Task type", "TaskType": "Task type",
"ManageProjects": "Project types" "ManageProjects": "Project types",
"CreateProjectType": "Create project type",
"ClassicProject": "Classic project"
} }
} }

View File

@ -81,6 +81,8 @@
"StatusChange": "Статус изменен", "StatusChange": "Статус изменен",
"TaskCreated": "Создана задача", "TaskCreated": "Создана задача",
"TaskType": "Тип задачи", "TaskType": "Тип задачи",
"ManageProjects": "Управление проектами" "ManageProjects": "Управление проектами",
"CreateProjectType": "Создать тип проекта",
"ClassicProject": "Классический проект"
} }
} }

View File

@ -1,17 +1,15 @@
<script lang="ts"> <script lang="ts">
import { Attribute, Class, IdMap, Ref, Status, generateId } from '@hcengineering/core' import { Class, IdMap, Ref, Status, generateId } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { DocPopup, createQuery, getClient } from '@hcengineering/presentation' import { DocPopup, getClient } from '@hcengineering/presentation'
import { Project, ProjectType, Task, getStates } from '@hcengineering/task' import { Task, TaskType } from '@hcengineering/task'
import { ObjectPresenter, statusStore } from '@hcengineering/view-resources' import { ObjectPresenter, statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { typeStore } from '..' import { taskTypeStore } from '..'
import task from '../plugin'
export let value: Task | Task[] export let value: Task | Task[]
export let width: 'medium' | 'large' | 'full' = 'medium' export let width: 'medium' | 'large' | 'full' = 'medium'
export let placeholder: IntlString export let placeholder: IntlString
export let ofAttribute: Ref<Attribute<Status>>
export let _class: Ref<Class<Status>> export let _class: Ref<Class<Status>>
export let embedded: boolean = false export let embedded: boolean = false
@ -43,35 +41,26 @@
: undefined : undefined
: value.status : value.status
$: _space = Array.isArray(value) $: kind = Array.isArray(value)
? value.every((v) => v.space === (value as Array<Task>)[0].space) ? value.every((v) => v.kind === (value as Array<Task>)[0].kind)
? value[0].space ? value[0].kind
: undefined : undefined
: value.space : value.kind
let project: Project | undefined function updateStatuses (taskTypes: IdMap<TaskType>, store: IdMap<Status>, kind: Ref<TaskType> | undefined): void {
if (kind === undefined) {
const query = createQuery() statuses = []
$: _space
? query.query(task.class.Project, { _id: _space as Ref<Project> }, (res) => {
project = res[0]
})
: (project = undefined)
function updateStatuses (
space: Project | undefined,
types: IdMap<ProjectType>,
store: IdMap<Status>,
allStatuses: Status[]
): void {
if (space === undefined) {
statuses = allStatuses.filter((p) => p.ofAttribute === ofAttribute)
} else { } else {
statuses = getStates(space, types, store) if (kind !== undefined) {
const type = taskTypes.get(kind)
if (type !== undefined) {
statuses = type.statuses.map((p) => store.get(p)).filter((p) => p !== undefined) as Status[]
}
}
} }
} }
$: updateStatuses(project, $typeStore, $statusStore.byId, $statusStore.array) $: updateStatuses($taskTypeStore, $statusStore.byId, kind)
let statuses: Status[] = [] let statuses: Status[] = []
</script> </script>

View File

@ -0,0 +1,77 @@
<!--
// 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, generateId } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { ProjectTypeDescriptor, createProjectType } from '@hcengineering/task'
import { DropdownLabelsIntl, EditBox, ToggleWithLabel } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
const client = getClient()
let name: string = ''
let classic: boolean = true
let descriptor: ProjectTypeDescriptor | undefined = undefined
const dispatch = createEventDispatcher()
async function createType (): Promise<void> {
if (descriptor === undefined) {
return
}
await createProjectType(
client,
{
name,
descriptor: descriptor._id,
description: '',
tasks: [],
classic
},
[],
generateId()
)
dispatch('close')
}
const descriptors = client.getModel().findAllSync(task.class.ProjectTypeDescriptor, {})
const items = descriptors.map((it) => ({
label: it.name,
id: it._id
}))
function selectType (evt: CustomEvent<Ref<ProjectTypeDescriptor>>): void {
descriptor = descriptors.find((it) => it._id === evt.detail)
}
</script>
<Card
label={task.string.CreateProjectType}
canSave={name.trim().length > 0 && descriptor !== undefined}
okAction={createType}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<div class="flex-col flex-gap-2">
<EditBox bind:value={name} placeholder={task.string.ProjectType} />
<DropdownLabelsIntl {items} on:selected={selectType} />
<ToggleWithLabel label={task.string.ClassicProject} bind:on={classic} />
</div>
</Card>

View File

@ -14,46 +14,12 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { generateId } from '@hcengineering/core' import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import { Button, IconAdd, Menu, getEventPopupPositionElement, showPopup } from '@hcengineering/ui' import CreateProjectType from './CreateProjectType.svelte'
import { translate } from '@hcengineering/platform' function open () {
import { getClient } from '@hcengineering/presentation' showPopup(CreateProjectType, {}, 'top')
import task, { ProjectTypeDescriptor, createProjectType } from '@hcengineering/task'
const client = getClient()
async function createType (descriptor: ProjectTypeDescriptor): Promise<void> {
const descriptorName = await translate(descriptor.name, {})
await createProjectType(
client,
{
name: `New ${descriptorName} project type`,
descriptor: descriptor._id,
description: '',
tasks: []
},
[],
generateId()
)
}
async function chooseProjectType (evt: MouseEvent): Promise<void> {
const descriptors = client.getModel().findAllSync(task.class.ProjectTypeDescriptor, {})
showPopup(
Menu,
{
actions: descriptors.map((it) => ({
label: it.name,
action: () => {
void createType(it)
}
}))
},
getEventPopupPositionElement(evt)
)
} }
</script> </script>
<Button id="new-project-type" icon={IconAdd} kind={'link'} size="small" on:click={chooseProjectType} /> <Button id="new-project-type" icon={IconAdd} kind={'link'} size="small" on:click={open} />

View File

@ -75,7 +75,9 @@ export default mergeIds(taskId, task, {
StatusPopupTitle: '' as IntlString, StatusPopupTitle: '' as IntlString,
NameAlreadyExists: '' as IntlString, NameAlreadyExists: '' as IntlString,
StatusChange: '' as IntlString, StatusChange: '' as IntlString,
TaskCreated: '' as IntlString TaskCreated: '' as IntlString,
CreateProjectType: '' as IntlString,
ClassicProject: '' as IntlString
}, },
status: { status: {
AssigneeRequired: '' as IntlString AssigneeRequired: '' as IntlString

View File

@ -173,6 +173,9 @@ export interface ProjectType extends Space {
// A mixin for project // A mixin for project
targetClass: Ref<Class<Project>> targetClass: Ref<Class<Project>>
// disable automation workflow
classic: boolean
} }
/** /**

View File

@ -255,7 +255,8 @@ export async function createProjectType (
members: [], members: [],
archived: false, archived: false,
statuses: calculateStatuses({ tasks: _tasks, statuses: [] }, tasksData, []), statuses: calculateStatuses({ tasks: _tasks, statuses: [] }, tasksData, []),
targetClass: targetProjectClassId targetClass: targetProjectClassId,
classic: data.classic
}, },
_id _id
) )

View File

@ -86,10 +86,10 @@ test.describe('contact tests', () => {
await page.getByRole('button', { name: 'Notifications' }).click() await page.getByRole('button', { name: 'Notifications' }).click()
// Click text=Vacancies // Click text=Vacancies
await page.locator('#new-project-type').click() await page.locator('#new-project-type').click()
await page.getByRole('button', { name: 'Recruiting', exact: true }).click()
await page.locator('#navGroup-statuses').getByText('New Recruiting project type').first().click()
// TODO: Need rework. // TODO: Need rework.
// await page.getByRole('button', { name: 'Recruiting', exact: true }).click()
// await page.locator('#navGroup-statuses').getByText('New Recruiting project type').first().click()
// // Click #create-template div // // Click #create-template div
// await page.click('#create-template div') // await page.click('#create-template div')
// const tid = 'template-' + generateId() // const tid = 'template-' + generateId()