Tracker: Project - Editors (#1779)

Signed-off-by: Artyom Grigorovich <grigorovichartyom@gmail.com>
This commit is contained in:
Artyom Grigorovich 2022-05-18 12:35:34 +07:00 committed by GitHub
parent c6b757d67c
commit ae13a1ff2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1079 additions and 205 deletions

View File

@ -58,6 +58,8 @@
let search: string = ''
let objects: Doc[] = []
$: selectedElements = new Set(selectedObjects)
const dispatch = createEventDispatcher()
const query = createQuery()
$: query.query<Doc>(
@ -73,15 +75,18 @@
{ ...(options ?? {}), limit: 200 }
)
const isSelected = (person: Doc): boolean => {
if (selectedObjects.filter((p) => p === person._id).length > 0) return true
return false
}
const checkSelected = (person: Doc, objects: Doc[]): void => {
selectedObjects = isSelected(person)
? selectedObjects.filter((p) => p !== person._id)
: [...selectedObjects, person._id]
if (selectedElements.has(person._id)) {
selectedElements.delete(person._id)
} else {
selectedElements.add(person._id)
}
selectedObjects = Array.from(selectedElements)
dispatch('update', selectedObjects)
selectedElements = selectedElements
}
const client = getClient()
@ -168,7 +173,7 @@
>
{#if multiSelect}
<div class="check pointer-events-none">
<CheckBox checked={isSelected(obj)} primary />
<CheckBox checked={selectedElements.has(obj._id)} primary />
</div>
{/if}

View File

@ -231,6 +231,16 @@
background-color: var(--button-disabled-color);
border-color: transparent;
}
&.selected {
background-color: var(--button-bg-hover);
border-color: var(--button-border-hover);
color: var(--caption-color);
.btn-icon {
color: var(--accent-color);
}
}
}
&.no-border {
font-weight: 400;
@ -275,10 +285,10 @@
}
&.link-bordered {
padding: 0 0.375rem;
color: var(--acctent-color);
color: var(--accent-color);
border-color: var(--button-border-color);
&:hover {
color: var(--acctent-color);
color: var(--accent-color);
border-color: var(--button-border-hover);
.btn-icon {
color: var(--accent-color);

View File

@ -1,11 +1,11 @@
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
// It should be published with your NPM package. It should not be tracked by Git.
{
"tsdocVersion": "0.12",
"toolPackages": [
{
"packageName": "@microsoft/api-extractor",
"packageVersion": "7.23.0"
}
]
}
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
// It should be published with your NPM package. It should not be tracked by Git.
{
"tsdocVersion": "0.12",
"toolPackages": [
{
"packageName": "@microsoft/api-extractor",
"packageVersion": "7.23.0"
}
]
}

View File

@ -8,10 +8,12 @@
export let value: WithLookup<Employee>
export let shouldShowAvatar: boolean = true
export let shouldShowName: boolean = true
export let onEmployeeEdit: ((event: MouseEvent) => void) | undefined = undefined
let container: HTMLElement
function onEdit () {
const onEdit = () => {
showPopup(
EmployeePreviewPopup,
{
@ -20,11 +22,13 @@
container
)
}
$: handlePersonEdit = onEmployeeEdit ?? onEdit
</script>
<div bind:this={container} class="flex-center container">
<div class="pr-2 over-underline">
<PersonPresenter {value} {onEdit} {shouldShowAvatar} />
<div class="over-underline" class:pr-2={shouldShowName}>
<PersonPresenter {value} onEdit={handlePersonEdit} {shouldShowAvatar} {shouldShowName} />
</div>
{#if value.$lookup?.statuses?.length}
<div class="status content-color">

View File

@ -100,7 +100,7 @@
}
}
.eContentPresenterIcon {
margin-right: 0.5rem;
margin-right: 0.25rem;
color: var(--theme-content-dark-color);
}
.eContentPresenterLabel {

View File

@ -94,4 +94,20 @@
<rect x="6" y="5" width="3" height="9" rx="1" fill-opacity=".4" />
<rect x="11" y="2" width="3" height="12" rx="1" fill-opacity=".4" />
</symbol>
<symbol id="list" viewBox="0 0 16 16">
<path d="M1 1.8C1 1.51997 1 1.37996 1.0545 1.273C1.10243 1.17892 1.17892 1.10243 1.273 1.0545C1.37996 1 1.51997 1 1.8 1H14.2C14.48 1 14.62 1 14.727 1.0545C14.8211 1.10243 14.8976 1.17892 14.9455 1.273C15 1.37996 15 1.51997 15 1.8V2.2C15 2.48003 15 2.62004 14.9455 2.727C14.8976 2.82108 14.8211 2.89757 14.727 2.9455C14.62 3 14.48 3 14.2 3H1.8C1.51997 3 1.37996 3 1.273 2.9455C1.17892 2.89757 1.10243 2.82108 1.0545 2.727C1 2.62004 1 2.48003 1 2.2V1.8Z" />
<path d="M1 13.8C1 13.52 1 13.38 1.0545 13.273C1.10243 13.1789 1.17892 13.1024 1.273 13.0545C1.37996 13 1.51997 13 1.8 13H14.2C14.48 13 14.62 13 14.727 13.0545C14.8211 13.1024 14.8976 13.1789 14.9455 13.273C15 13.38 15 13.52 15 13.8V14.2C15 14.48 15 14.62 14.9455 14.727C14.8976 14.8211 14.8211 14.8976 14.727 14.9455C14.62 15 14.48 15 14.2 15H1.8C1.51997 15 1.37996 15 1.273 14.9455C1.17892 14.8976 1.10243 14.8211 1.0545 14.727C1 14.62 1 14.48 1 14.2V13.8Z" />
<path d="M1 9.8C1 9.51997 1 9.37996 1.0545 9.273C1.10243 9.17892 1.17892 9.10243 1.273 9.0545C1.37996 9 1.51997 9 1.8 9H14.2C14.48 9 14.62 9 14.727 9.0545C14.8211 9.10243 14.8976 9.17892 14.9455 9.273C15 9.37996 15 9.51997 15 9.8V10.2C15 10.48 15 10.62 14.9455 10.727C14.8976 10.8211 14.8211 10.8976 14.727 10.9455C14.62 11 14.48 11 14.2 11H1.8C1.51997 11 1.37996 11 1.273 10.9455C1.17892 10.8976 1.10243 10.8211 1.0545 10.727C1 10.62 1 10.48 1 10.2V9.8Z" />
<path d="M1 5.8C1 5.51997 1 5.37996 1.0545 5.273C1.10243 5.17892 1.17892 5.10243 1.273 5.0545C1.37996 5 1.51997 5 1.8 5H14.2C14.48 5 14.62 5 14.727 5.0545C14.8211 5.10243 14.8976 5.17892 14.9455 5.273C15 5.37996 15 5.51997 15 5.8V6.2C15 6.48003 15 6.62004 14.9455 6.727C14.8976 6.82108 14.8211 6.89757 14.727 6.9455C14.62 7 14.48 7 14.2 7H1.8C1.51997 7 1.37996 7 1.273 6.9455C1.17892 6.89757 1.10243 6.82108 1.0545 6.727C1 6.62004 1 6.48003 1 6.2V5.8Z" />
</symbol>
<symbol id="timeline" viewBox="0 0 14 14">
<rect x="6" y="1" width="7" height="2.5" rx="0.5" />
<rect x="1" y="5.75" width="9" height="2.5" rx="0.5" />
<rect x="4" y="10.5" width="9" height="2.5" rx="0.5" />
</symbol>
<symbol id="projectMembers" viewBox="0 0 16 16">
<path d="M1 3C1 1.89543 1.89543 1 3 1H9C10.1046 1 11 1.89543 11 3V3.5H6C4.61929 3.5 3.5 4.61929 3.5 6V11H3C1.89543 11 1 10.1046 1 9V3Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C5.89543 5 5 5.89543 5 7V13C5 14.1046 5.89543 15 7 15H13C14.1046 15 15 14.1046 15 13V7C15 5.89543 14.1046 5 13 5H7ZM10 10C10.9665 10 11.5 9.2165 11.5 8.25C11.5 7.2835 10.9665 6.5 10 6.5C9.0335 6.5 8.5 7.2835 8.5 8.25C8.5 9.2165 9.0335 10 10 10ZM7 12.5616C7 11.5144 7.9841 10.746 9 11C9.47572 11.7136 10.5243 11.7136 11 11C12.0159 10.746 13 11.5144 13 12.5616V13.0101C13 13.2806 12.7806 13.5 12.5101 13.5H7.48995C7.21936 13.5 7 13.2806 7 13.0101V12.5616Z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -17,6 +17,10 @@
"Backlog": "Backlog",
"Board": "Board",
"Projects": "Projects",
"AllProjects": "All",
"BacklogProjects": "Backlog",
"ActiveProjects": "Active",
"ClosedProjects": "Closed",
"NewProject": "New project",
"CreateProject": "Create project",
"ProjectNamePlaceholder": "Project name",
@ -101,6 +105,10 @@
"AddToProject": "Add to project\u2026",
"MoveToProject": "Move to project\u2026",
"NoProject": "No project",
"ProjectLeadTitle": "Project lead",
"ProjectMembersTitle": "Project members",
"ProjectLeadSearchPlaceholder": "Set project lead\u2026",
"ProjectMembersSearchPlaceholder": "Change project members\u2026",
"GotoIssues": "Go to issues",
"GotoActive": "Go to active issues",

View File

@ -45,7 +45,11 @@ loadMetadata(tracker.icon, {
PriorityUrgent: `${icons}#priority-urgent`,
PriorityHigh: `${icons}#priority-high`,
PriorityMedium: `${icons}#priority-medium`,
PriorityLow: `${icons}#priority-low`
PriorityLow: `${icons}#priority-low`,
ProjectsList: `${icons}#list`,
ProjectsTimeline: `${icons}#timeline`,
ProjectMembers: `${icons}#projectMembers`
})
addStringsLoader(trackerId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,72 @@
<!--
// 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 { Timestamp } from '@anticrm/core'
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
import DueDatePopup from './DueDatePopup.svelte'
import { getDueDateIconModifier } from '../utils'
export let dateMs: number | null = null
export let shouldRender: boolean = true
export let onDateChange: (newDate: number | null) => void
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: isOverdue = dateMs !== null && dateMs < today.getTime()
$: dueDate = dateMs === null ? null : new Date(dateMs)
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
$: formattedDate = !dateMs ? '' : new Date(dateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail
if (newDate === undefined || dateMs === newDate) {
return
}
onDateChange(newDate)
}
</script>
{#if shouldRender}
{#if formattedDate}
<Tooltip
direction={'top'}
component={DueDatePopup}
props={{
formattedDate: formattedDate,
daysDifference: daysDifference,
isOverdue: isOverdue,
iconModifier: iconModifier
}}
>
<DatePresenter
value={dateMs}
editable={true}
shouldShowLabel={false}
icon={iconModifier}
on:change={handleDueDateChanged}
/>
</Tooltip>
{:else}
<DatePresenter
value={dateMs}
editable={true}
shouldShowLabel={false}
icon={iconModifier}
on:change={handleDueDateChanged}
/>
{/if}
{/if}

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Icon, Label, IconDPCalendarOver, IconDPCalendar } from '@anticrm/ui'
import tracker from '../../plugin'
import tracker from '../plugin'
export let formattedDate: string = ''
export let daysDifference: number = 0

View File

@ -14,7 +14,6 @@
-->
<script lang="ts">
import { Ref, WithLookup } from '@anticrm/core'
import { IssueStatus } from '@anticrm/tracker'
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'

View File

@ -67,6 +67,7 @@
if (!isEditable) {
return
}
showPopup(
UsersPopup,
{
@ -90,7 +91,7 @@
isInteractive={true}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEdit={handleAssigneeEditorOpened}
onEmployeeEdit={handleAssigneeEditorOpened}
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
/>
{/if}

View File

@ -13,59 +13,30 @@
// limitations under the License.
-->
<script lang="ts">
import { Timestamp, WithLookup } from '@anticrm/core'
import { 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 CommonTrackerDatePresenter from '../CommonTrackerDatePresenter.svelte'
import tracker from '../../plugin'
import { getDueDateIconModifier } from '../../utils'
export let value: WithLookup<Issue>
const client = getClient()
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: dueDateMs = value.dueDate
$: isOverdue = dueDateMs !== null && dueDateMs < today.getTime()
$: dueDate = dueDateMs === null ? null : new Date(dueDateMs)
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
$: iconModifier = getDueDateIconModifier(isOverdue, daysDifference)
$: formattedDate = !dueDateMs ? '' : new Date(dueDateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail
if (newDate === undefined || value.dueDate === newDate) {
return
}
const handleDueDateChanged = async (newDate: number | null) => {
await client.update(value, { dueDate: newDate })
}
$: shouldRenderPresenter =
dueDateMs &&
dueDateMs !== null &&
value.$lookup?.status?.category !== tracker.issueStatusCategory.Completed &&
value.$lookup?.status?.category !== tracker.issueStatusCategory.Canceled
</script>
{#if shouldRenderPresenter}
<Tooltip
direction={'top'}
component={DueDatePopup}
props={{
formattedDate: formattedDate,
daysDifference: daysDifference,
isOverdue: isOverdue,
iconModifier: iconModifier
}}
>
<DatePresenter
value={dueDateMs}
editable={true}
shouldShowLabel={false}
icon={iconModifier}
on:change={handleDueDateChanged}
/>
</Tooltip>
{/if}
<CommonTrackerDatePresenter
dateMs={dueDateMs}
shouldRender={shouldRenderPresenter}
onDateChange={handleDueDateChanged}
/>

View File

@ -0,0 +1,23 @@
<!--
// 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 { WithLookup } from '@anticrm/core'
import { Project } from '@anticrm/tracker'
import { Button } from '@anticrm/ui'
export let value: WithLookup<Project>
</script>
<Button size="small" kind="link" icon={value.icon} />

View File

@ -0,0 +1,71 @@
<!--
// 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 { Employee } from '@anticrm/contact'
import { Avatar } from '@anticrm/presentation'
import { Label } from '@anticrm/ui'
import tracker from '../../plugin'
export let lead: Employee
</script>
<div class="root">
<div class="icon">
<Avatar avatar={lead.avatar} size="medium" />
</div>
<div class="textContainer">
<div class="title">
<Label label={tracker.string.ProjectLeadTitle} />
</div>
<div class="description">
{lead.name}
</div>
</div>
</div>
<style lang="scss">
.root {
display: flex;
width: 20rem;
background-color: var(--board-card-bg-color);
}
.icon {
display: inline-block;
line-height: 0;
flex-shrink: 0;
font-size: 14px;
}
.textContainer {
display: flex;
flex: 1 1 auto;
flex-direction: column;
margin-top: -0.125rem;
margin-left: 0.75rem;
}
.title {
color: var(--content-color);
font-weight: 500;
}
.description {
display: flex;
flex-flow: row wrap;
min-width: 0;
margin-top: 0.375rem;
}
</style>

View File

@ -0,0 +1,111 @@
<!--
// 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 contact, { Employee } from '@anticrm/contact'
import { Class, Doc, Ref } from '@anticrm/core'
import { Project, Team } from '@anticrm/tracker'
import { UsersPopup, getClient } from '@anticrm/presentation'
import { AttributeModel } from '@anticrm/view'
import { eventToHTMLElement, showPopup, Tooltip } from '@anticrm/ui'
import { getObjectPresenter } from '@anticrm/view-resources'
import { IntlString } from '@anticrm/platform'
import tracker from '../../plugin'
import LeadPopup from './LeadPopup.svelte'
export let value: Employee | null
export let projectId: Ref<Project>
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
export let currentSpace: Ref<Team> | undefined = undefined
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
export let defaultName: IntlString | undefined = undefined
const client = getClient()
let presenter: AttributeModel | undefined
$: if (value || defaultClass) {
if (value) {
getObjectPresenter(client, value._class, { key: '' }).then((p) => {
presenter = p
})
} else if (defaultClass) {
getObjectPresenter(client, defaultClass, { key: '' }).then((p) => {
presenter = p
})
}
}
const handleLeadChanged = async (result: Employee | null | undefined) => {
if (!isEditable || result === undefined) {
return
}
const currentProject = await client.findOne(tracker.class.Project, { space: currentSpace, _id: projectId })
if (currentProject === undefined) {
return
}
const newLead = result === null ? null : result._id
await client.update(currentProject, { lead: newLead })
}
const handleLeadEditorOpened = async (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
UsersPopup,
{
_class: contact.class.Employee,
selected: value?._id,
allowDeselect: true,
placeholder: tracker.string.ProjectLeadSearchPlaceholder
},
eventToHTMLElement(event),
handleLeadChanged
)
}
</script>
{#if value && presenter}
<Tooltip component={LeadPopup} props={{ lead: value }}>
<svelte:component
this={presenter.presenter}
{value}
{defaultName}
avatarSize={'tiny'}
isInteractive={true}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEmployeeEdit={handleLeadEditorOpened}
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
/>
</Tooltip>
{:else if presenter}
<svelte:component
this={presenter.presenter}
{value}
{defaultName}
avatarSize={'tiny'}
isInteractive={true}
shouldShowPlaceholder={true}
shouldShowName={shouldShowLabel}
onEmployeeEdit={handleLeadEditorOpened}
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
/>
{/if}

View File

@ -44,6 +44,14 @@
async function onSave () {
await client.createDoc(tracker.class.Project, space, object)
}
const handleProjectStatusChanged = (newProjectStatus: ProjectStatus | undefined) => {
if (newProjectStatus === undefined) {
return
}
object.status = newProjectStatus
}
</script>
<Card
@ -74,12 +82,7 @@
/>
</div>
<div slot="pool" class="flex-row-center text-sm gap-1-5">
<ProjectStatusSelector
bind:status={object.status}
onStatusChange={(newStatus) => {
newStatus !== undefined && (object.status = newStatus)
}}
/>
<ProjectStatusSelector selectedProjectStatus={object.status} onProjectStatusChange={handleProjectStatusChanged} />
<UserBox
_class={contact.class.Employee}
label={tracker.string.ProjectLead}

View File

@ -0,0 +1,72 @@
<!--
// 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 { Project } from '@anticrm/tracker'
import { Button, showPopup, eventToHTMLElement } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import contact, { Employee } from '@anticrm/contact'
import { getClient, UsersPopup } from '@anticrm/presentation'
import { translate } from '@anticrm/platform'
import tracker from '../../plugin'
export let value: Project
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
const client = getClient()
let buttonTitle = ''
$: translate(tracker.string.ProjectMembersTitle, {}).then((res) => {
buttonTitle = res
})
const handleProjectMembersChanged = async (result: Ref<Employee>[] | undefined) => {
if (result === undefined) {
return
}
await client.update(value, { members: result })
}
const handleProjectMembersEditorOpened = async (event: MouseEvent) => {
showPopup(
UsersPopup,
{
_class: contact.class.Employee,
selectedUsers: value.members,
allowDeselect: true,
multiSelect: true,
placeholder: tracker.string.ProjectMembersSearchPlaceholder
},
eventToHTMLElement(event),
undefined,
handleProjectMembersChanged
)
}
</script>
<Button
{kind}
{size}
{width}
{justify}
title={buttonTitle}
icon={tracker.icon.ProjectMembers}
on:click={handleProjectMembersEditorOpened}
/>

View File

@ -13,61 +13,31 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, WithLookup } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Icon } from '@anticrm/ui'
import contact from '@anticrm/contact'
import ObjectPresenter from '@anticrm/view-resources/src/components/ObjectPresenter.svelte'
import { Project, ProjectStatus, Team } from '@anticrm/tracker'
import ProjectStatusSelector from './ProjectStatusSelector.svelte'
import tracker from '../../plugin'
import { WithLookup } from '@anticrm/core'
import { Project } from '@anticrm/tracker'
export let value: WithLookup<Project>
export let space: Ref<Team>
const client = getClient()
const lead = value.$lookup?.lead
async function updateStatus (status: ProjectStatus) {
await client.updateDoc(tracker.class.Project, space, value._id, { status })
}
</script>
<div class="flex-presenter">
<div class="icon">
<Icon icon={value.icon} size="small" />
{#if value}
<div class="flex-presenter projectPresenterRoot">
<span title={value.label} class="projectLabel">{value.label}</span>
</div>
<span class="label nowrap project-label">{value.label}</span>
{#if lead}
<div class="lead-container">
<ObjectPresenter value={lead} objectId={lead._id} _class={lead._class} props={{ shouldShowName: false }} />
</div>
{:else}
<div class="lead-placeholder">
<Icon icon={contact.icon.Person} size="large" />
</div>
{/if}
<div class="icon">
<ProjectStatusSelector
kind="icon"
shouldShowLabel={false}
status={value.status}
onStatusChange={(status) => status !== undefined && updateStatus(status)}
/>
</div>
</div>
{/if}
<style>
.project-label {
width: 250px;
<style lang="scss">
.projectPresenterRoot {
max-width: 5rem;
}
.lead-container {
padding-bottom: 5px;
margin-right: 12px;
}
.lead-placeholder {
margin-right: 20px;
.projectLabel {
display: block;
min-width: 0;
font-weight: 500;
text-align: left;
color: var(--theme-caption-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,69 @@
<!--
// 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 { Project, ProjectStatus } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import { Tooltip } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import tracker from '../../plugin'
import ProjectStatusSelector from './ProjectStatusSelector.svelte'
export let value: Project
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = '100%'
const client = getClient()
const handleProjectStatusChanged = async (newStatus: ProjectStatus | undefined) => {
if (!isEditable || newStatus === undefined || value.status === newStatus) {
return
}
await client.update(value, { status: newStatus })
}
</script>
{#if value}
{#if isEditable}
<Tooltip label={tracker.string.SetStatus} fill>
<ProjectStatusSelector
{kind}
{size}
{width}
{justify}
{isEditable}
{shouldShowLabel}
selectedProjectStatus={value.status}
onProjectStatusChange={handleProjectStatusChanged}
/>
</Tooltip>
{:else}
<ProjectStatusSelector
{kind}
{size}
{width}
{justify}
{isEditable}
{shouldShowLabel}
selectedProjectStatus={value.status}
onProjectStatusChange={handleProjectStatusChanged}
/>
{/if}
{/if}

View File

@ -13,26 +13,35 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, Icon, Label, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
import { ProjectStatus } from '@anticrm/tracker'
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import tracker from '../../plugin'
import { projectStatuses } from '../../utils'
import { defaultProjectStatuses, projectStatusAssets } from '../../utils'
export let status: ProjectStatus
export let kind: 'button' | 'icon' = 'button'
export let selectedProjectStatus: ProjectStatus | undefined
export let shouldShowLabel: boolean = true
export let onStatusChange: ((newStatus: ProjectStatus | undefined) => void) | undefined = undefined
export let onProjectStatusChange: ((newProjectStatus: ProjectStatus | undefined) => void) | undefined = undefined
export let isEditable: boolean = true
const statusesInfo = [
ProjectStatus.Planned,
ProjectStatus.InProgress,
ProjectStatus.Paused,
ProjectStatus.Completed,
ProjectStatus.Canceled
].map((s) => ({ id: s, ...projectStatuses[s] }))
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
const handleStatusEditorOpened = (event: MouseEvent) => {
$: selectedStatusIcon = selectedProjectStatus
? projectStatusAssets[selectedProjectStatus].icon
: tracker.icon.CategoryBacklog
$: selectedStatusLabel = shouldShowLabel
? selectedProjectStatus
? projectStatusAssets[selectedProjectStatus].label
: tracker.string.Planned
: undefined
$: statusesInfo = defaultProjectStatuses.map((s) => ({ id: s, ...projectStatusAssets[s] }))
const handleProjectStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
@ -40,42 +49,18 @@
SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
eventToHTMLElement(event),
onStatusChange
onProjectStatusChange
)
}
</script>
{#if kind === 'button'}
<Button
label={shouldShowLabel ? projectStatuses[status].label : undefined}
icon={projectStatuses[status].icon}
width="min-content"
size="small"
kind="no-border"
on:click={handleStatusEditorOpened}
/>
{:else if kind === 'icon'}
<div class={isEditable ? 'flex-presenter' : 'presenter'} on:click={handleStatusEditorOpened}>
<div class="statusIcon">
<Icon icon={projectStatuses[status].icon} size="small" />
</div>
{#if shouldShowLabel}
<div class="label nowrap ml-2">
<Label label={projectStatuses[status].label} />
</div>
{/if}
</div>
{/if}
<style lang="scss">
.presenter {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.statusIcon {
width: 1rem;
height: 1rem;
}
</style>
<Button
icon={selectedStatusIcon}
label={selectedStatusLabel}
{justify}
{width}
{size}
{kind}
disabled={!isEditable}
on:click={handleProjectStatusEditorOpened}
/>

View File

@ -13,60 +13,156 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@anticrm/core'
import { Team } from '@anticrm/tracker'
import { Button, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import plugin from '../../plugin'
import contact from '@anticrm/contact'
import { DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation'
import { Project, Team } from '@anticrm/tracker'
import { Button, IconAdd, IconOptions, Label, showPopup } from '@anticrm/ui'
import tracker from '../../plugin'
import NewProject from './NewProject.svelte'
import ProjectsListBrowser from './ProjectsListBrowser.svelte'
export let space: Ref<Team>
export let currentSpace: Ref<Team>
export let title: IntlString = tracker.string.AllProjects
export let query: DocumentQuery<Project> = {}
export let search: string = ''
async function showCreateDialog () {
showPopup(NewProject, { space, targetElement: null }, null)
const ENTRIES_LIMIT = 200
const resultProjectsQuery = createQuery()
const projectOptions: FindOptions<Project> = {
sort: { modifiedOn: SortingOrder.Descending },
limit: ENTRIES_LIMIT,
lookup: { lead: contact.class.Employee, members: contact.class.Employee }
}
let resultProjects: Project[] = []
$: baseQuery = {
space: currentSpace,
...query
}
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
$: resultProjectsQuery.query<Project>(
tracker.class.Project,
{ ...resultQuery },
(result) => {
resultProjects = result
},
projectOptions
)
const showCreateDialog = async () => {
showPopup(NewProject, { space: currentSpace, targetElement: null }, null)
}
</script>
<div>
<div class="header">
<div class="header-left">
<Label label={plugin.string.Projects} />
<div class="fs-title flex-between header">
<div class="flex-center">
<Label label={tracker.string.Projects} />
<div class="projectTitle">
<Label label={title} />
</div>
</div>
<div class="header-right">
<Button icon={IconAdd} label={plugin.string.Project} kind="secondary" on:click={showCreateDialog} />
<Button size="small" icon={IconAdd} label={tracker.string.Project} kind="secondary" on:click={showCreateDialog} />
</div>
<div class="itemsContainer">
<div class="flex-center">
<div class="flex-center">
<div class="buttonWrapper">
<Button selected size="small" shape="rectangle-right" label={tracker.string.AllProjects} />
</div>
<div class="buttonWrapper">
<Button size="small" shape="rectangle" label={tracker.string.BacklogProjects} />
</div>
<div class="buttonWrapper">
<Button size="small" shape="rectangle" label={tracker.string.ActiveProjects} />
</div>
<div class="buttonWrapper">
<Button size="small" shape="rectangle-left" label={tracker.string.ClosedProjects} />
</div>
</div>
<div class="ml-3 filterButton">
<Button
size="small"
icon={IconAdd}
kind={'link-bordered'}
borderStyle={'dashed'}
label={tracker.string.Filter}
on:click={() => {}}
/>
</div>
</div>
<div class="flex-center">
<div class="flex-center">
<div class="buttonWrapper">
<Button selected size="small" shape="rectangle-right" icon={tracker.icon.ProjectsList} />
</div>
<div class="buttonWrapper">
<Button size="small" shape="rectangle-left" icon={tracker.icon.ProjectsTimeline} />
</div>
</div>
<div class="ml-3">
<Button size="small" icon={IconOptions} on:click={() => {}} />
</div>
</div>
</div>
<Table
_class={plugin.class.Project}
config={[
<ProjectsListBrowser
_class={tracker.class.Project}
itemsConfig={[
{ key: '', presenter: tracker.component.IconPresenter },
{ key: '', presenter: tracker.component.ProjectPresenter },
{
key: '',
presenter: plugin.component.ProjectPresenter,
label: plugin.string.Project,
sortingKey: 'name',
props: { space }
}
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { currentSpace, defaultClass: contact.class.Employee, shouldShowLabel: false }
},
{ key: '', presenter: tracker.component.ProjectMembersPresenter, props: { kind: 'link' } },
{ key: '', presenter: tracker.component.TargetDatePresenter },
{ key: '', presenter: tracker.component.ProjectStatusPresenter }
]}
query={{}}
projects={resultProjects}
/>
</div>
<style>
<style lang="scss">
.header {
width: 100%;
height: 50px;
min-height: 3.5rem;
padding-left: 2.25rem;
padding-right: 0.5rem;
border-bottom: 1px solid var(--theme-button-border-hovered);
}
.projectTitle {
display: flex;
margin-left: 0.25rem;
color: var(--content-color);
font-size: 0.8125rem;
font-weight: 500;
}
.itemsContainer {
display: flex;
border-bottom: 0.5px solid #666666;
align-items: center;
justify-content: space-between;
padding: 0.65rem 1.35rem 0.65rem 2.25rem;
border-bottom: 1px solid var(--theme-button-border-hovered);
}
.header-left {
margin-left: 10px;
.buttonWrapper {
margin-right: 1px;
&:last-child {
margin-right: 0;
}
}
.header-right {
margin-right: 10px;
.filterButton {
color: var(--caption-color);
}
</style>

View File

@ -0,0 +1,246 @@
<!--
// 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 contact from '@anticrm/contact'
import { Class, Doc, FindOptions, getObjectValue, Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Issue, Project } from '@anticrm/tracker'
import { CheckBox, Spinner, Tooltip } from '@anticrm/ui'
import { AttributeModel, BuildModelKey } from '@anticrm/view'
import { buildModel, getObjectPresenter, LoadingProps } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
export let _class: Ref<Class<Doc>>
export let itemsConfig: (BuildModelKey | string)[]
export let selectedObjectIds: Doc[] = []
export let selectedRowIndex: number | undefined = undefined
export let projects: Project[] | undefined = undefined
export let loadingProps: LoadingProps | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
const objectRefs: HTMLElement[] = []
const baseOptions: FindOptions<Issue> = {
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus
}
}
let personPresenter: AttributeModel
$: options = { ...baseOptions } as FindOptions<Project>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = projects?.length ?? 0
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
personPresenter = p
})
export const onObjectChecked = (docs: Doc[], value: boolean) => {
dispatch('check', { docs, value })
}
const handleRowFocused = (object: Doc) => {
dispatch('row-focus', object)
}
export const onElementSelected = (offset: 1 | -1 | 0, docObject?: Doc) => {
if (!projects) {
return
}
let position =
(docObject !== undefined ? projects?.findIndex((x) => x._id === docObject?._id) : selectedRowIndex) ?? -1
position += offset
if (position < 0) {
position = 0
}
if (position >= projects.length) {
position = projects.length - 1
}
const objectRef = objectRefs[position]
selectedRowIndex = position
handleRowFocused(projects[position])
if (objectRef) {
objectRef.scrollIntoView({ behavior: 'auto', block: 'nearest' })
}
}
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
if (options?.limit && options?.limit > 0) {
return Math.min(options.limit, props.length)
}
return props.length
}
</script>
{#await buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }) then itemModels}
<div class="listRoot">
{#if projects}
{#each projects as docObject (docObject._id)}
<div
bind:this={objectRefs[projects.findIndex((x) => x === docObject)]}
class="listGrid"
class:mListGridChecked={selectedObjectIdsSet.has(docObject._id)}
class:mListGridFixed={selectedRowIndex === projects.findIndex((x) => x === docObject)}
class:mListGridSelected={selectedRowIndex === projects.findIndex((x) => x === docObject)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
>
<div class="contentWrapper">
{#each itemModels as attributeModel, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<Tooltip direction={'bottom'} label={tracker.string.SelectIssue}>
<div class="eListGridCheckBox">
<CheckBox
checked={selectedObjectIdsSet.has(docObject._id)}
on:value={(event) => {
onObjectChecked([docObject], event.detail)
}}
/>
</div>
</Tooltip>
<div class="iconPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="projectPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
<div class="filler" />
{:else}
<div class="gridElement">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
issueId={docObject._id}
{...attributeModel.props}
/>
</div>
{/if}
{/each}
</div>
</div>
{/each}
{:else if loadingProps !== undefined}
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
<div class="listGrid" class:fixed={rowIndex === selectedRowIndex}>
<div class="contentWrapper">
<div class="gridElement">
<CheckBox checked={false} />
<div class="ml-4">
<Spinner size="small" />
</div>
</div>
</div>
</div>
{/each}
{/if}
</div>
{/await}
<style lang="scss">
.listRoot {
width: 100%;
}
.contentWrapper {
display: flex;
align-items: center;
height: 100%;
padding-left: 0.75rem;
padding-right: 1.15rem;
}
.listGrid {
width: 100%;
height: 3.25rem;
color: var(--theme-caption-color);
border-bottom: 1px solid var(--theme-button-border-hovered);
&.mListGridChecked {
background-color: var(--theme-table-bg-hover);
.eListGridCheckBox {
opacity: 1;
}
}
&.mListGridSelected {
background-color: var(--menu-bg-select);
}
.eListGridCheckBox {
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
&:hover {
opacity: 1;
}
}
}
.filler {
display: flex;
flex-grow: 1;
}
.gridElement {
display: flex;
align-items: center;
justify-content: flex-start;
margin-left: 0.5rem;
&:first-child {
margin-left: 0;
}
}
.iconPresenter {
padding-left: 0.45rem;
}
.projectPresenter {
display: flex;
align-items: center;
flex-shrink: 0;
width: 5.5rem;
margin-left: 0.5rem;
}
</style>

View File

@ -0,0 +1,72 @@
<!--
// 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 type { Class, Doc, Ref } from '@anticrm/core'
import { BuildModelKey } from '@anticrm/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
SelectDirection,
selectionStore,
LoadingProps
} from '@anticrm/view-resources'
import { Project } from '@anticrm/tracker'
import { onMount } from 'svelte'
import ProjectsList from './ProjectsList.svelte'
export let _class: Ref<Class<Doc>>
export let itemsConfig: (BuildModelKey | string)[]
export let loadingProps: LoadingProps | undefined = undefined
export let projects: Project[] = []
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
projectsList.onElementSelected(offset, of)
}
})
let projectsList: ProjectsList
$: if (projectsList !== undefined) {
listProvider.update(projects)
}
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<ProjectsList
bind:this={projectsList}
{_class}
{itemsConfig}
{loadingProps}
{projects}
selectedObjectIds={$selectionStore ?? []}
selectedRowIndex={listProvider.current($focusStore)}
on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined)
}}
on:check={(event) => {
listProvider.updateSelection(event.detail.docs, event.detail.value)
}}
/>

View File

@ -0,0 +1,31 @@
<!--
// 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 { Project } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import CommonTrackerDatePresenter from '../CommonTrackerDatePresenter.svelte'
export let value: Project
const client = getClient()
$: dueDateMs = value.targetDate
const handleDueDateChanged = async (newDate: number | null) => {
await client.update(value, { targetDate: newDate })
}
</script>
<CommonTrackerDatePresenter dateMs={dueDateMs} shouldRender={true} onDateChange={handleDueDateChanged} />

View File

@ -37,6 +37,11 @@ import StatusEditor from './components/issues/StatusEditor.svelte'
import DueDatePresenter from './components/issues/DueDatePresenter.svelte'
import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
import ViewOptionsPopup from './components/issues/ViewOptionsPopup.svelte'
import IconPresenter from './components/projects/IconPresenter.svelte'
import LeadPresenter from './components/projects/LeadPresenter.svelte'
import TargetDatePresenter from './components/projects/TargetDatePresenter.svelte'
import ProjectMembersPresenter from './components/projects/ProjectMembersPresenter.svelte'
import ProjectStatusPresenter from './components/projects/ProjectStatusPresenter.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
import EditIssue from './components/issues/edit/EditIssue.svelte'
@ -67,6 +72,11 @@ export default async (): Promise<Resources> => ({
DueDatePresenter,
EditIssue,
NewIssueHeader,
ViewOptionsPopup
ViewOptionsPopup,
IconPresenter,
LeadPresenter,
TargetDatePresenter,
ProjectMembersPresenter,
ProjectStatusPresenter
}
})

View File

@ -36,6 +36,10 @@ export default mergeIds(trackerId, tracker, {
Board: '' as IntlString,
Project: '' as IntlString,
Projects: '' as IntlString,
AllProjects: '' as IntlString,
BacklogProjects: '' as IntlString,
ActiveProjects: '' as IntlString,
ClosedProjects: '' as IntlString,
NewProject: '' as IntlString,
CreateProject: '' as IntlString,
ProjectNamePlaceholder: '' as IntlString,
@ -119,6 +123,10 @@ export default mergeIds(trackerId, tracker, {
AddToProject: '' as IntlString,
MoveToProject: '' as IntlString,
NoProject: '' as IntlString,
ProjectLeadTitle: '' as IntlString,
ProjectMembersTitle: '' as IntlString,
ProjectLeadSearchPlaceholder: '' as IntlString,
ProjectMembersSearchPlaceholder: '' as IntlString,
IssueTitlePlaceholder: '' as IntlString,
IssueDescriptionPlaceholder: '' as IntlString,
@ -160,6 +168,11 @@ export default mergeIds(trackerId, tracker, {
DueDatePresenter: '' as AnyComponent,
EditIssue: '' as AnyComponent,
CreateTeam: '' as AnyComponent,
NewIssueHeader: '' as AnyComponent
NewIssueHeader: '' as AnyComponent,
IconPresenter: '' as AnyComponent,
LeadPresenter: '' as AnyComponent,
TargetDatePresenter: '' as AnyComponent,
ProjectMembersPresenter: '' as AnyComponent,
ProjectStatusPresenter: '' as AnyComponent
}
})

View File

@ -114,8 +114,16 @@ export const getIssuesModificationDatePeriodTime = (period: IssuesDateModificati
}
}
export const defaultProjectStatuses = [
ProjectStatus.Planned,
ProjectStatus.InProgress,
ProjectStatus.Paused,
ProjectStatus.Completed,
ProjectStatus.Canceled
]
// TODO: update icons
export const projectStatuses: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
export const projectStatusAssets: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
[ProjectStatus.Planned]: { icon: tracker.icon.CategoryBacklog, label: tracker.string.Planned },
[ProjectStatus.InProgress]: { icon: tracker.icon.CategoryStarted, label: tracker.string.InProgress },
[ProjectStatus.Paused]: { icon: tracker.icon.CategoryUnstarted, label: tracker.string.Paused },

View File

@ -226,6 +226,10 @@ export default plugin(trackerId, {
PriorityUrgent: '' as Asset,
PriorityHigh: '' as Asset,
PriorityMedium: '' as Asset,
PriorityLow: '' as Asset
PriorityLow: '' as Asset,
ProjectsList: '' as Asset,
ProjectsTimeline: '' as Asset,
ProjectMembers: '' as Asset
}
})