Tracker: Project - Project selector (#1740)

Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
Artyom Grigorovich 2022-05-16 17:38:51 +07:00 committed by GitHub
parent 0231234f9a
commit 84844b65a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 297 additions and 93 deletions

View File

@ -148,6 +148,9 @@ export class TIssue extends TDoc implements Issue {
@Prop(TypeRef(contact.class.Employee), tracker.string.Assignee) @Prop(TypeRef(contact.class.Employee), tracker.string.Assignee)
assignee!: Ref<Employee> | null assignee!: Ref<Employee> | null
@Prop(TypeRef(tracker.class.Project), tracker.string.Project)
project!: Ref<Project> | null
@Prop(TypeRef(tracker.class.Issue), tracker.string.Parent) @Prop(TypeRef(tracker.class.Issue), tracker.string.Parent)
parentIssue!: Ref<Issue> parentIssue!: Ref<Issue>
@ -212,6 +215,9 @@ export class TProject extends TDoc implements Project {
@Prop(TypeMarkup(), tracker.string.Project) @Prop(TypeMarkup(), tracker.string.Project)
description?: Markup description?: Markup
@Prop(TypeString(), tracker.string.AssetLabel)
icon!: Asset
@Prop(TypeNumber(), tracker.string.Status) @Prop(TypeNumber(), tracker.string.Status)
status!: ProjectStatus status!: ProjectStatus

View File

@ -16,6 +16,7 @@
import core, { generateId, Ref, TxOperations } from '@anticrm/core' import core, { generateId, Ref, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model' import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { IssueStatus, IssueStatusCategory, Team, genRanks } from '@anticrm/tracker' import { IssueStatus, IssueStatusCategory, Team, genRanks } from '@anticrm/tracker'
import { DOMAIN_TRACKER } from '.'
import tracker from './plugin' import tracker from './plugin'
enum DeprecatedIssueStatus { enum DeprecatedIssueStatus {
@ -148,6 +149,36 @@ async function upgradeIssueStatuses (tx: TxOperations): Promise<void> {
} }
} }
async function migrateIssueProjects (client: MigrationClient): Promise<void> {
const issues = await client.find(DOMAIN_TRACKER, { _class: tracker.class.Issue, project: { $exists: false } })
if (issues.length === 0) {
return
}
for (const issue of issues) {
await client.update(DOMAIN_TRACKER, { _id: issue._id }, { project: null })
}
}
async function upgradeProjectIcons (tx: TxOperations): Promise<void> {
const projects = await tx.findAll(tracker.class.Project, {})
if (projects.length === 0) {
return
}
for (const project of projects) {
const icon = project.icon as unknown
if (icon !== undefined) {
continue
}
await tx.update(project, { icon: tracker.icon.Projects })
}
}
async function createDefaults (tx: TxOperations): Promise<void> { async function createDefaults (tx: TxOperations): Promise<void> {
await createDefaultTeam(tx) await createDefaultTeam(tx)
} }
@ -160,12 +191,19 @@ async function upgradeIssues (tx: TxOperations): Promise<void> {
await upgradeIssueStatuses(tx) await upgradeIssueStatuses(tx)
} }
async function upgradeProjects (tx: TxOperations): Promise<void> {
await upgradeProjectIcons(tx)
}
export const trackerOperation: MigrateOperation = { export const trackerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {}, async migrate (client: MigrationClient): Promise<void> {
await Promise.all([migrateIssueProjects(client)])
},
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)
await createDefaults(tx) await createDefaults(tx)
await upgradeTeams(tx) await upgradeTeams(tx)
await upgradeIssues(tx) await upgradeIssues(tx)
await upgradeProjects(tx)
} }
} }

View File

@ -16,7 +16,7 @@
import type { Asset, IntlString } from '@anticrm/platform' import type { Asset, IntlString } from '@anticrm/platform'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { registerFocus } from '../focus' import { registerFocus } from '../focus'
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '../types' import type { AnySvelteComponent, ButtonKind, ButtonShape, ButtonSize } from '../types'
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import Label from './Label.svelte' import Label from './Label.svelte'
import Spinner from './Spinner.svelte' import Spinner from './Spinner.svelte'
@ -25,7 +25,7 @@
export let labelParams: Record<string, any> = {} export let labelParams: Record<string, any> = {}
export let kind: ButtonKind = 'secondary' export let kind: ButtonKind = 'secondary'
export let size: ButtonSize = 'medium' export let size: ButtonSize = 'medium'
export let shape: 'rectangle' | 'rectangle-left' | 'rectangle-right' | 'circle' | 'round' | undefined = undefined export let shape: ButtonShape = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let justify: 'left' | 'center' = 'center' export let justify: 'left' | 'center' = 'center'
export let disabled: boolean = false export let disabled: boolean = false

View File

@ -65,6 +65,7 @@ export type TabModel = Tab[]
export type ButtonKind = 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'link-bordered' | 'dangerous' export type ButtonKind = 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'link-bordered' | 'dangerous'
export type ButtonSize = 'small' | 'medium' | 'large' | 'x-large' export type ButtonSize = 'small' | 'medium' | 'large' | 'x-large'
export type ButtonShape = 'rectangle' | 'rectangle-left' | 'rectangle-right' | 'circle' | 'round' | undefined
export interface PopupPositionElement { export interface PopupPositionElement {
getBoundingClientRect: () => DOMRect getBoundingClientRect: () => DOMRect
position?: { position?: {

View File

@ -96,6 +96,10 @@
"PastMonth": "Past month", "PastMonth": "Past month",
"CopyIssueUrl": "Copy Issue URL to clipboard", "CopyIssueUrl": "Copy Issue URL to clipboard",
"CopyIssueId": "Copy Issue ID to clipboard", "CopyIssueId": "Copy Issue ID to clipboard",
"AssetLabel": "Asset",
"AddToProject": "Add to project\u2026",
"MoveToProject": "Move to project\u2026",
"NoProject": "No project",
"GotoIssues": "Go to issues", "GotoIssues": "Go to issues",
"GotoActive": "Go to active issues", "GotoActive": "Go to active issues",

View File

@ -17,7 +17,7 @@
import core, { Data, generateId, Ref, SortingOrder, WithLookup } from '@anticrm/core' import core, { Data, generateId, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { Asset, IntlString } from '@anticrm/platform' import { Asset, IntlString } from '@anticrm/platform'
import presentation, { getClient, UserBox, Card, createQuery } from '@anticrm/presentation' import presentation, { getClient, UserBox, Card, createQuery } from '@anticrm/presentation'
import { Issue, IssuePriority, IssueStatus, Team, calcRank } from '@anticrm/tracker' import { Issue, IssuePriority, IssueStatus, Team, calcRank, Project } from '@anticrm/tracker'
import { StyledTextBox } from '@anticrm/text-editor' import { StyledTextBox } from '@anticrm/text-editor'
import { import {
EditBox, EditBox,
@ -32,20 +32,23 @@
import tracker from '../plugin' import tracker from '../plugin'
import StatusSelector from './StatusSelector.svelte' import StatusSelector from './StatusSelector.svelte'
import PrioritySelector from './PrioritySelector.svelte' import PrioritySelector from './PrioritySelector.svelte'
import ProjectSelector from './ProjectSelector.svelte'
export let space: Ref<Team> export let space: Ref<Team>
export let parent: Ref<Issue> | undefined export let parent: Ref<Issue> | undefined
export let status: Ref<IssueStatus> | undefined = undefined export let status: Ref<IssueStatus> | undefined = undefined
export let priority: IssuePriority = IssuePriority.NoPriority export let priority: IssuePriority = IssuePriority.NoPriority
export let assignee: Ref<Employee> | null = null export let assignee: Ref<Employee> | null = null
export let project: Ref<Project> | null = null
let currentAssignee: Ref<Employee> | null = assignee let currentAssignee: Ref<Employee> | null = assignee
let issueStatuses: WithLookup<IssueStatus>[] = [] let issueStatuses: WithLookup<IssueStatus>[] = []
const object: Data<Issue> = { let object: Data<Issue> = {
title: '', title: '',
description: '', description: '',
assignee: null, assignee: null,
project: project,
number: 0, number: 0,
rank: '', rank: '',
status: '' as Ref<IssueStatus>, status: '' as Ref<IssueStatus>,
@ -62,6 +65,7 @@
$: _space = space $: _space = space
$: _parent = parent $: _parent = parent
$: updateIssueStatusId(space, status) $: updateIssueStatusId(space, status)
$: statusesQuery.query( $: statusesQuery.query(
tracker.class.IssueStatus, tracker.class.IssueStatus,
{ attachedTo: space }, { attachedTo: space },
@ -73,6 +77,7 @@
sort: { rank: SortingOrder.Ascending } sort: { rank: SortingOrder.Ascending }
} }
) )
$: canSave = getTitle(object.title ?? '').length > 0 $: canSave = getTitle(object.title ?? '').length > 0
async function updateIssueStatusId (teamId: Ref<Team>, issueStatusId?: Ref<IssueStatus>) { async function updateIssueStatusId (teamId: Ref<Team>, issueStatusId?: Ref<IssueStatus>) {
@ -125,6 +130,7 @@
title: getTitle(object.title), title: getTitle(object.title),
description: object.description, description: object.description,
assignee: currentAssignee, assignee: currentAssignee,
project: object.project,
number: (incResult as any).object.sequence, number: (incResult as any).object.sequence,
status: object.status, status: object.status,
priority: object.priority, priority: object.priority,
@ -151,9 +157,19 @@
} }
const handleStatusChanged = (statusId: Ref<IssueStatus> | undefined) => { const handleStatusChanged = (statusId: Ref<IssueStatus> | undefined) => {
if (statusId !== undefined) { if (statusId === undefined) {
object.status = statusId return
} }
object.status = statusId
}
const handleProjectIdChanged = (projectId: Ref<Project> | null | undefined) => {
if (projectId === undefined) {
return
}
object = { ...object, project: projectId }
} }
</script> </script>
@ -212,13 +228,7 @@
size="small" size="small"
kind="no-border" kind="no-border"
/> />
<Button <ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
label={tracker.string.Project}
icon={tracker.icon.Projects}
width="min-content"
size="small"
kind="no-border"
/>
<DatePresenter bind:value={object.dueDate} editable /> <DatePresenter bind:value={object.dueDate} editable />
<Button <Button
icon={tracker.icon.MoreActions} icon={tracker.icon.MoreActions}

View File

@ -0,0 +1,109 @@
<!--
// 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 { Ref, SortingOrder } from '@anticrm/core'
import { Project } from '@anticrm/tracker'
import { IntlString, translate } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { Button, showPopup, SelectPopup, eventToHTMLElement, ButtonShape } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import tracker from '../plugin'
export let value: Ref<Project> | null | undefined
export let shouldShowLabel: boolean = true
export let isEditable: boolean = true
export let onProjectIdChange: ((newProjectId: Ref<Project> | undefined) => void) | undefined = undefined
export let popupPlaceholder: IntlString = tracker.string.AddToProject
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let shape: ButtonShape = undefined
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
const client = getClient()
const projectsQuery = createQuery()
let projects: Project[] = []
let selectedProject: Project | undefined
let defaultProjectLabel = ''
$: projectsQuery.query(
tracker.class.Project,
{},
(currentProjects) => {
projects = currentProjects
},
{
sort: { modifiedOn: SortingOrder.Ascending }
}
)
$: if (value !== undefined) {
handleSelectedProjectIdUpdated(value)
}
$: translate(tracker.string.Project, {}).then((result) => (defaultProjectLabel = result))
$: projectIcon = selectedProject?.icon ?? tracker.icon.Projects
$: projectText = shouldShowLabel ? selectedProject?.label ?? defaultProjectLabel : undefined
$: projectsInfo = [
{ id: null, icon: tracker.icon.Projects, label: tracker.string.NoProject },
...projects.map((p) => ({
id: p._id,
icon: p.icon,
text: p.label
}))
]
const handleSelectedProjectIdUpdated = async (newProjectId: Ref<Project> | null) => {
if (newProjectId === null) {
selectedProject = undefined
return
}
selectedProject = await client.findOne(tracker.class.Project, { _id: newProjectId })
}
const handleProjectEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
SelectPopup,
{ value: projectsInfo, placeholder: popupPlaceholder, searchable: true },
eventToHTMLElement(event),
onProjectIdChange
)
}
</script>
<Button
{kind}
{size}
{shape}
{width}
{justify}
icon={projectIcon}
disabled={!isEditable}
on:click={handleProjectEditorOpened}
>
<svelte:fragment slot="content">
{#if projectText}
<span class="nowrap">{projectText}</span>
{/if}
</svelte:fragment>
</Button>

View File

@ -161,7 +161,7 @@
{object.title} {object.title}
</span> </span>
<div class="flex gap-2 mt-2 mb-2"> <div class="flex gap-2 mt-2 mb-2">
<PriorityEditor value={issue} {currentSpace} isEditable={true} /> <PriorityEditor value={issue} isEditable={true} />
</div> </div>
</div> </div>
</svelte:fragment> </svelte:fragment>

View File

@ -13,15 +13,14 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, Timestamp, WithLookup } from '@anticrm/core' import { Timestamp, WithLookup } from '@anticrm/core'
import { Issue, Team } from '@anticrm/tracker' import { Issue } from '@anticrm/tracker'
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui' import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import DueDatePopup from './DueDatePopup.svelte' import DueDatePopup from './DueDatePopup.svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
export let currentSpace: Ref<Team> | undefined = undefined
const WARNING_DAYS = 7 const WARNING_DAYS = 7
const client = getClient() const client = getClient()
@ -37,17 +36,11 @@
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => { const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail const newDate = event.detail
if (newDate === undefined) { if (newDate === undefined || value.dueDate === newDate) {
return return
} }
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id }) await client.update(value, { dueDate: newDate })
if (currentIssue === undefined) {
return
}
await client.update(currentIssue, { dueDate: newDate })
} }
const getIconModifier = (isOverdue: boolean, daysDifference: number | null) => { const getIconModifier = (isOverdue: boolean, daysDifference: number | null) => {

View File

@ -15,7 +15,6 @@
<script lang="ts"> <script lang="ts">
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import { Class, Ref, SortingOrder, WithLookup } from '@anticrm/core' import { Class, Ref, SortingOrder, WithLookup } from '@anticrm/core'
// import Card from '../Card.svelte'
import { Panel } from '@anticrm/panel' import { Panel } from '@anticrm/panel'
import { createQuery, getClient, UserBox } from '@anticrm/presentation' import { createQuery, getClient, UserBox } from '@anticrm/presentation'
import { StyledTextBox } from '@anticrm/text-editor' import { StyledTextBox } from '@anticrm/text-editor'
@ -34,15 +33,16 @@
import tracker from '../../plugin' import tracker from '../../plugin'
import IssuePresenter from './IssuePresenter.svelte' import IssuePresenter from './IssuePresenter.svelte'
import PriorityEditor from './PriorityEditor.svelte' import PriorityEditor from './PriorityEditor.svelte'
import ProjectEditor from './ProjectEditor.svelte'
import StatusEditor from './StatusEditor.svelte' import StatusEditor from './StatusEditor.svelte'
export let _id: Ref<Issue> export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>> export let _class: Ref<Class<Issue>>
const query = createQuery()
const statusesQuery = createQuery()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
const query = createQuery()
const statusesQuery = createQuery()
let issue: Issue | undefined let issue: Issue | undefined
let currentTeam: Team | undefined let currentTeam: Team | undefined
@ -173,13 +173,11 @@
<span class="label"> <span class="label">
<Label label={tracker.string.Status} /> <Label label={tracker.string.Status} />
</span> </span>
<StatusEditor value={issue} statuses={issueStatuses} currentSpace={currentTeam._id} shouldShowLabel /> <StatusEditor value={issue} statuses={issueStatuses} shouldShowLabel />
<span class="label"> <span class="label">
<Label label={tracker.string.Priority} /> <Label label={tracker.string.Priority} />
</span> </span>
<PriorityEditor value={issue} currentSpace={currentTeam._id} shouldShowLabel /> <PriorityEditor value={issue} shouldShowLabel />
<span class="label"> <span class="label">
<Label label={tracker.string.Assignee} /> <Label label={tracker.string.Assignee} />
</span> </span>
@ -214,18 +212,9 @@
<span class="label"> <span class="label">
<Label label={tracker.string.Project} /> <Label label={tracker.string.Project} />
</span> </span>
<Button <ProjectEditor value={issue} />
label={tracker.string.Project}
icon={tracker.icon.Projects}
size={'large'}
kind={'link'}
width={'100%'}
justify={'left'}
/>
{#if issue.dueDate !== null} {#if issue.dueDate !== null}
<div class="divider" /> <div class="divider" />
<span class="label"> <span class="label">
<Label label={tracker.string.DueDate} /> <Label label={tracker.string.DueDate} />
</span> </span>
@ -241,13 +230,7 @@
size="small" size="small"
kind="no-border" kind="no-border"
/> />
<Button <ProjectEditor value={issue} size={'small'} kind={'no-border'} width={'min-content'} />
label={tracker.string.Project}
icon={tracker.icon.Projects}
width="min-content"
size="small"
kind="no-border"
/>
</div> </div>
{/if} {/if}
</svelte:fragment> </svelte:fragment>

View File

@ -478,11 +478,16 @@
{employees} {employees}
categories={displayedCategories} categories={displayedCategories}
itemsConfig={[ itemsConfig={[
{ key: '', presenter: tracker.component.PriorityEditor, props: { currentSpace } }, { key: '', presenter: tracker.component.PriorityEditor },
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } }, { key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
{ key: '', presenter: tracker.component.StatusEditor, props: { currentSpace, statuses } }, { key: '', presenter: tracker.component.StatusEditor, props: { statuses } },
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } }, { key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } }, { key: '', presenter: tracker.component.DueDatePresenter },
{
key: '',
presenter: tracker.component.ProjectEditor,
props: { kind: 'secondary', size: 'small', shape: 'round', shouldShowPlaceholder: false }
},
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter }, { key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
{ {
key: '$lookup.assignee', key: '$lookup.assignee',

View File

@ -13,8 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Ref } from '@anticrm/core' import { Issue, IssuePriority } from '@anticrm/tracker'
import { Issue, IssuePriority, Team } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { Tooltip } from '@anticrm/ui' import { Tooltip } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui' import type { ButtonKind, ButtonSize } from '@anticrm/ui'
@ -22,7 +21,6 @@
import PrioritySelector from '../PrioritySelector.svelte' import PrioritySelector from '../PrioritySelector.svelte'
export let value: Issue export let value: Issue
export let currentSpace: Ref<Team> | undefined = undefined
export let isEditable: boolean = true export let isEditable: boolean = true
export let shouldShowLabel: boolean = false export let shouldShowLabel: boolean = false
@ -34,17 +32,11 @@
const client = getClient() const client = getClient()
const handlePriorityChanged = async (newPriority: IssuePriority | undefined) => { const handlePriorityChanged = async (newPriority: IssuePriority | undefined) => {
if (!isEditable || newPriority === undefined) { if (!isEditable || newPriority === undefined || value.priority === newPriority) {
return return
} }
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id }) await client.update(value, { priority: newPriority })
if (currentIssue === undefined) {
return
}
await client.update(currentIssue, { priority: newPriority })
} }
</script> </script>

View File

@ -0,0 +1,59 @@
<!--
// 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 { Ref } from '@anticrm/core'
import { Issue, Project } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import type { ButtonKind, ButtonShape, ButtonSize } from '@anticrm/ui'
import tracker from '../../plugin'
import ProjectSelector from '../ProjectSelector.svelte'
import { IntlString } from '@anticrm/platform'
export let value: Issue
export let isEditable: boolean = true
export let shouldShowLabel: boolean = true
export let popupPlaceholder: IntlString = tracker.string.MoveToProject
export let shouldShowPlaceholder = true
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let shape: ButtonShape = undefined
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = '100%'
const client = getClient()
const handleProjectIdChanged = async (newProjectId: Ref<Project> | null | undefined) => {
if (!isEditable || newProjectId === undefined || value.project === newProjectId) {
return
}
await client.update(value, { project: newProjectId })
}
</script>
{#if value.project || shouldShowPlaceholder}
<ProjectSelector
{kind}
{size}
{shape}
{width}
{justify}
{isEditable}
{shouldShowLabel}
{popupPlaceholder}
value={value.project}
onProjectIdChange={handleProjectIdChanged}
/>
{/if}

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, WithLookup } from '@anticrm/core' import { Ref, WithLookup } from '@anticrm/core'
import { Issue, IssueStatus, Team } from '@anticrm/tracker' import { Issue, IssueStatus } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { Tooltip } from '@anticrm/ui' import { Tooltip } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui' import type { ButtonKind, ButtonSize } from '@anticrm/ui'
@ -23,7 +23,6 @@
export let value: Issue export let value: Issue
export let statuses: WithLookup<IssueStatus>[] export let statuses: WithLookup<IssueStatus>[]
export let currentSpace: Ref<Team> | undefined = undefined
export let isEditable: boolean = true export let isEditable: boolean = true
export let shouldShowLabel: boolean = false export let shouldShowLabel: boolean = false
@ -35,17 +34,11 @@
const client = getClient() const client = getClient()
const handleStatusChanged = async (newStatus: Ref<IssueStatus> | undefined) => { const handleStatusChanged = async (newStatus: Ref<IssueStatus> | undefined) => {
if (!isEditable || newStatus === undefined) { if (!isEditable || newStatus === undefined || value.status === newStatus) {
return return
} }
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id }) await client.update(value, { status: newStatus })
if (currentIssue === undefined) {
return
}
await client.update(currentIssue, { status: newStatus })
} }
</script> </script>

View File

@ -20,7 +20,7 @@
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import { Project, ProjectStatus, Team } from '@anticrm/tracker' import { Project, ProjectStatus, Team } from '@anticrm/tracker'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin' import tracker from '../../plugin'
import ProjectStatusSelector from './ProjectStatusSelector.svelte' import ProjectStatusSelector from './ProjectStatusSelector.svelte'
export let space: Ref<Team> export let space: Ref<Team>
@ -30,6 +30,7 @@
const object: Data<Project> = { const object: Data<Project> = {
label: '' as IntlString, label: '' as IntlString,
description: '', description: '',
icon: tracker.icon.Projects,
status: ProjectStatus.Planned, status: ProjectStatus.Planned,
lead: null, lead: null,
members: [], members: [],
@ -41,25 +42,25 @@
} }
async function onSave () { async function onSave () {
await client.createDoc(plugin.class.Project, space, object) await client.createDoc(tracker.class.Project, space, object)
} }
</script> </script>
<Card <Card
label={plugin.string.NewProject} label={tracker.string.NewProject}
okAction={onSave} okAction={onSave}
canSave={object.label !== ''} canSave={object.label !== ''}
okLabel={plugin.string.CreateProject} okLabel={tracker.string.CreateProject}
spaceClass={plugin.class.Team} spaceClass={tracker.class.Team}
spaceLabel={plugin.string.Team} spaceLabel={tracker.string.Team}
spacePlaceholder={plugin.string.SelectTeam} spacePlaceholder={tracker.string.SelectTeam}
bind:space bind:space
on:close={() => dispatch('close')} on:close={() => dispatch('close')}
> >
<div class="label"> <div class="label">
<EditBox <EditBox
bind:value={object.label} bind:value={object.label}
placeholder={plugin.string.ProjectNamePlaceholder} placeholder={tracker.string.ProjectNamePlaceholder}
maxWidth="37.5rem" maxWidth="37.5rem"
kind="large-style" kind="large-style"
focus focus
@ -68,7 +69,7 @@
<div class="description"> <div class="description">
<EditBox <EditBox
bind:value={object.description} bind:value={object.description}
placeholder={plugin.string.ProjectDescriptionPlaceholder} placeholder={tracker.string.ProjectDescriptionPlaceholder}
maxWidth="37.5rem" maxWidth="37.5rem"
kind="editbox" kind="editbox"
/> />
@ -82,20 +83,20 @@
/> />
<UserBox <UserBox
_class={contact.class.Employee} _class={contact.class.Employee}
label={plugin.string.ProjectLead} label={tracker.string.ProjectLead}
placeholder={plugin.string.AssignTo} placeholder={tracker.string.AssignTo}
bind:value={object.lead} bind:value={object.lead}
allowDeselect allowDeselect
titleDeselect={plugin.string.Unassigned} titleDeselect={tracker.string.Unassigned}
/> />
<UserBoxList <UserBoxList
_class={contact.class.Employee} _class={contact.class.Employee}
bind:items={object.members} bind:items={object.members}
label={plugin.string.ProjectStatusPlaceholder} label={tracker.string.ProjectStatusPlaceholder}
/> />
<!-- TODO: add labels after customize IssueNeedsToBeCompletedByThisDate --> <!-- TODO: add labels after customize IssueNeedsToBeCompletedByThisDate -->
<DatePresenter bind:value={object.startDate} labelNull={plugin.string.StartDate} editable /> <DatePresenter bind:value={object.startDate} labelNull={tracker.string.StartDate} editable />
<DatePresenter bind:value={object.targetDate} labelNull={plugin.string.TargetDate} editable /> <DatePresenter bind:value={object.targetDate} labelNull={tracker.string.TargetDate} editable />
</div> </div>
</Card> </Card>

View File

@ -19,8 +19,8 @@
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import ObjectPresenter from '@anticrm/view-resources/src/components/ObjectPresenter.svelte' import ObjectPresenter from '@anticrm/view-resources/src/components/ObjectPresenter.svelte'
import { Project, ProjectStatus, Team } from '@anticrm/tracker' import { Project, ProjectStatus, Team } from '@anticrm/tracker'
import plugin from '../../plugin'
import ProjectStatusSelector from './ProjectStatusSelector.svelte' import ProjectStatusSelector from './ProjectStatusSelector.svelte'
import tracker from '../../plugin'
export let value: WithLookup<Project> export let value: WithLookup<Project>
export let space: Ref<Team> export let space: Ref<Team>
@ -29,13 +29,13 @@
const lead = value.$lookup?.lead const lead = value.$lookup?.lead
async function updateStatus (status: ProjectStatus) { async function updateStatus (status: ProjectStatus) {
await client.updateDoc(plugin.class.Project, space, value._id, { status }) await client.updateDoc(tracker.class.Project, space, value._id, { status })
} }
</script> </script>
<div class="flex-presenter"> <div class="flex-presenter">
<div class="icon"> <div class="icon">
<Icon icon={plugin.icon.Project} size="small" /> <Icon icon={value.icon} size="small" />
</div> </div>
<span class="label nowrap project-label">{value.label}</span> <span class="label nowrap project-label">{value.label}</span>
{#if lead} {#if lead}

View File

@ -44,7 +44,8 @@
key: '', key: '',
presenter: plugin.component.ProjectPresenter, presenter: plugin.component.ProjectPresenter,
label: plugin.string.Project, label: plugin.string.Project,
sortingKey: 'name' sortingKey: 'name',
props: { space }
} }
]} ]}
query={{}} query={{}}

View File

@ -30,6 +30,7 @@ import IssuePresenter from './components/issues/IssuePresenter.svelte'
import TitlePresenter from './components/issues/TitlePresenter.svelte' import TitlePresenter from './components/issues/TitlePresenter.svelte'
import PriorityPresenter from './components/issues/PriorityPresenter.svelte' import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
import PriorityEditor from './components/issues/PriorityEditor.svelte' import PriorityEditor from './components/issues/PriorityEditor.svelte'
import ProjectEditor from './components/issues/ProjectEditor.svelte'
import StatusPresenter from './components/issues/StatusPresenter.svelte' import StatusPresenter from './components/issues/StatusPresenter.svelte'
import StatusEditor from './components/issues/StatusEditor.svelte' import StatusEditor from './components/issues/StatusEditor.svelte'
import DueDatePresenter from './components/issues/DueDatePresenter.svelte' import DueDatePresenter from './components/issues/DueDatePresenter.svelte'
@ -57,6 +58,7 @@ export default async (): Promise<Resources> => ({
ModificationDatePresenter, ModificationDatePresenter,
PriorityPresenter, PriorityPresenter,
PriorityEditor, PriorityEditor,
ProjectEditor,
StatusPresenter, StatusPresenter,
StatusEditor, StatusEditor,
AssigneePresenter, AssigneePresenter,

View File

@ -115,6 +115,10 @@ export default mergeIds(trackerId, tracker, {
Filter: '' as IntlString, Filter: '' as IntlString,
ClearFilters: '' as IntlString, ClearFilters: '' as IntlString,
Back: '' as IntlString, Back: '' as IntlString,
AssetLabel: '' as IntlString,
AddToProject: '' as IntlString,
MoveToProject: '' as IntlString,
NoProject: '' as IntlString,
IssueTitlePlaceholder: '' as IntlString, IssueTitlePlaceholder: '' as IntlString,
IssueDescriptionPlaceholder: '' as IntlString, IssueDescriptionPlaceholder: '' as IntlString,
@ -145,6 +149,7 @@ export default mergeIds(trackerId, tracker, {
ModificationDatePresenter: '' as AnyComponent, ModificationDatePresenter: '' as AnyComponent,
PriorityPresenter: '' as AnyComponent, PriorityPresenter: '' as AnyComponent,
PriorityEditor: '' as AnyComponent, PriorityEditor: '' as AnyComponent,
ProjectEditor: '' as AnyComponent,
StatusPresenter: '' as AnyComponent, StatusPresenter: '' as AnyComponent,
StatusEditor: '' as AnyComponent, StatusEditor: '' as AnyComponent,
AssigneePresenter: '' as AnyComponent, AssigneePresenter: '' as AnyComponent,

View File

@ -103,6 +103,7 @@ export interface Issue extends Doc {
number: number number: number
assignee: Ref<Employee> | null assignee: Ref<Employee> | null
project: Ref<Project> | null
// For subtasks // For subtasks
parentIssue?: Ref<Issue> parentIssue?: Ref<Issue>
@ -149,6 +150,7 @@ export enum ProjectStatus {
export interface Project extends Doc { export interface Project extends Doc {
label: string label: string
description?: Markup description?: Markup
icon: Asset
status: ProjectStatus status: ProjectStatus