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

View File

@ -16,6 +16,7 @@
import core, { generateId, Ref, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { IssueStatus, IssueStatusCategory, Team, genRanks } from '@anticrm/tracker'
import { DOMAIN_TRACKER } from '.'
import tracker from './plugin'
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> {
await createDefaultTeam(tx)
}
@ -160,12 +191,19 @@ async function upgradeIssues (tx: TxOperations): Promise<void> {
await upgradeIssueStatuses(tx)
}
async function upgradeProjects (tx: TxOperations): Promise<void> {
await upgradeProjectIcons(tx)
}
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> {
const tx = new TxOperations(client, core.account.System)
await createDefaults(tx)
await upgradeTeams(tx)
await upgradeIssues(tx)
await upgradeProjects(tx)
}
}

View File

@ -16,7 +16,7 @@
import type { Asset, IntlString } from '@anticrm/platform'
import { onMount } from 'svelte'
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 Label from './Label.svelte'
import Spinner from './Spinner.svelte'
@ -25,7 +25,7 @@
export let labelParams: Record<string, any> = {}
export let kind: ButtonKind = 'secondary'
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 justify: 'left' | 'center' = 'center'
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 ButtonSize = 'small' | 'medium' | 'large' | 'x-large'
export type ButtonShape = 'rectangle' | 'rectangle-left' | 'rectangle-right' | 'circle' | 'round' | undefined
export interface PopupPositionElement {
getBoundingClientRect: () => DOMRect
position?: {

View File

@ -96,6 +96,10 @@
"PastMonth": "Past month",
"CopyIssueUrl": "Copy Issue URL 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",
"GotoActive": "Go to active issues",

View File

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

View File

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

View File

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

View File

@ -478,11 +478,16 @@
{employees}
categories={displayedCategories}
itemsConfig={[
{ key: '', presenter: tracker.component.PriorityEditor, props: { currentSpace } },
{ key: '', presenter: tracker.component.PriorityEditor },
{ 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.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: '$lookup.assignee',

View File

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

View File

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

View File

@ -19,8 +19,8 @@
import contact from '@anticrm/contact'
import ObjectPresenter from '@anticrm/view-resources/src/components/ObjectPresenter.svelte'
import { Project, ProjectStatus, Team } from '@anticrm/tracker'
import plugin from '../../plugin'
import ProjectStatusSelector from './ProjectStatusSelector.svelte'
import tracker from '../../plugin'
export let value: WithLookup<Project>
export let space: Ref<Team>
@ -29,13 +29,13 @@
const lead = value.$lookup?.lead
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>
<div class="flex-presenter">
<div class="icon">
<Icon icon={plugin.icon.Project} size="small" />
<Icon icon={value.icon} size="small" />
</div>
<span class="label nowrap project-label">{value.label}</span>
{#if lead}

View File

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

View File

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

View File

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

View File

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