Initial Sprints support (#2246)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-08-03 14:05:19 +07:00 committed by GitHub
parent 29775429d5
commit 00131ed0a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1715 additions and 113 deletions

View File

@ -2,6 +2,10 @@
## 0.6.32 (upcoming)
Tracker:
- Basic sprints
## 0.6.31
Core:

View File

@ -118,7 +118,6 @@ specifiers:
'@rush-temp/preference-assets': file:./projects/preference-assets.tgz
'@rush-temp/presentation': file:./projects/presentation.tgz
'@rush-temp/prod': file:./projects/prod.tgz
'@rush-temp/prod-tracker': file:./projects/prod-tracker.tgz
'@rush-temp/query': file:./projects/query.tgz
'@rush-temp/recruit': file:./projects/recruit.tgz
'@rush-temp/recruit-assets': file:./projects/recruit-assets.tgz
@ -221,6 +220,7 @@ specifiers:
'@types/node': ~16.11.12
'@types/pdfkit': ~0.12.3
'@types/request': ~2.48.8
'@types/sharp': ~0.30.4
'@types/tar-stream': ^2.2.2
'@types/toposort': ^2.0.3
'@types/uuid': ^8.3.1
@ -271,7 +271,6 @@ specifiers:
mini-css-extract-plugin: ^2.2.0
minio: ^7.0.26
mongodb: ^4.1.1
node-html-parser: ~5.3.3
pdfkit: ~0.13.0
postcss: ^8.3.4
postcss-load-config: ^3.1.0
@ -420,7 +419,6 @@ dependencies:
'@rush-temp/preference-assets': file:projects/preference-assets.tgz
'@rush-temp/presentation': file:projects/presentation.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c
'@rush-temp/prod': file:projects/prod.tgz_8b34f51e833a67e51e5bff1df3e73cc8
'@rush-temp/prod-tracker': file:projects/prod-tracker.tgz_8b34f51e833a67e51e5bff1df3e73cc8
'@rush-temp/query': file:projects/query.tgz
'@rush-temp/recruit': file:projects/recruit.tgz
'@rush-temp/recruit-assets': file:projects/recruit-assets.tgz_typescript@4.7.4
@ -523,6 +521,7 @@ dependencies:
'@types/node': 16.11.42
'@types/pdfkit': 0.12.6
'@types/request': 2.48.8
'@types/sharp': 0.30.4
'@types/tar-stream': 2.2.2
'@types/toposort': 2.0.3
'@types/uuid': 8.3.4
@ -573,7 +572,6 @@ dependencies:
mini-css-extract-plugin: 2.6.1_webpack@5.73.0
minio: 7.0.28
mongodb: 4.7.0
node-html-parser: 5.3.3
pdfkit: 0.13.0
postcss: 8.4.14
postcss-load-config: 3.1.4_postcss@8.4.14+ts-node@10.8.1
@ -12464,52 +12462,6 @@ packages:
- supports-color
dev: false
file:projects/prod-tracker.tgz_8b34f51e833a67e51e5bff1df3e73cc8:
resolution: {integrity: sha512-hOYYMh2Au/J1fHweEgyZbMhrUqWpIej9lA02kjW+gOHzxZZO68RTfPBJq1d+UoaHUWPtC+YxkUPUcGiBPEKgfw==, tarball: file:projects/prod-tracker.tgz}
id: file:projects/prod-tracker.tgz
name: '@rush-temp/prod-tracker'
version: 0.0.0
dependencies:
'@types/node': 16.11.42
autoprefixer: 10.4.7_postcss@8.4.14
compression-webpack-plugin: 9.0.1_webpack@5.73.0
cross-env: 7.0.3
css-loader: 5.2.7_webpack@5.73.0
dotenv-webpack: 7.1.1_webpack@5.73.0
file-loader: 6.2.0_webpack@5.73.0
html-webpack-plugin: 5.5.0_webpack@5.73.0
mini-css-extract-plugin: 2.6.1_webpack@5.73.0
postcss: 8.4.14
postcss-load-config: 3.1.4_postcss@8.4.14+ts-node@10.8.1
postcss-loader: 6.2.1_postcss@8.4.14+webpack@5.73.0
sass-loader: 12.6.0_sass@1.53.0+webpack@5.73.0
style-loader: 3.3.1_webpack@5.73.0
svelte: 3.48.0
svelte-loader: 3.1.3_svelte@3.48.0
svgo-loader: 3.0.1
ts-loader: 9.3.1_typescript@4.7.4+webpack@5.73.0
webpack: 5.73.0_0539a1ee9cc1e8c0305465d979f40a3d
webpack-bundle-analyzer: 4.5.0
webpack-cli: 4.10.0_7445a258404e01c9b84d81171e5727fd
webpack-dev-server: 4.9.3_ffd7cf999054608223b9fc836cf5004f
transitivePeerDependencies:
- '@swc/core'
- '@webpack-cli/generators'
- '@webpack-cli/migrate'
- bufferutil
- debug
- esbuild
- fibers
- node-sass
- sass
- sass-embedded
- supports-color
- ts-node
- typescript
- uglify-js
- utf-8-validate
dev: false
file:projects/prod.tgz_8b34f51e833a67e51e5bff1df3e73cc8:
resolution: {integrity: sha512-lOWf2/caG2bTwygiVfaeuB2EmI0qXT2J5cyKpc6ZXUPxwGul+YkWiiwjT6gzg0BGVEJssuN4owh7okSdrrlJdg==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
@ -14080,7 +14032,7 @@ packages:
dev: false
file:projects/tool.tgz:
resolution: {integrity: sha512-LjCQIq8dqLFEXvLcBmfJKRtkoJublg0r6DWC2RwIlmJyw61xEEBFHdOn3p1l3EyQPg0PjgAaSoEUPF+ViaxlPQ==, tarball: file:projects/tool.tgz}
resolution: {integrity: sha512-7BKMRQ8UZ8cgJGDXvMGNoAdjUmEAyMx9GVBmBo3gmW56eLhxamaBqdwkunRIemCR77MJ8JJj+qhgWAzpRXpoRg==, tarball: file:projects/tool.tgz}
name: '@rush-temp/tool'
version: 0.0.0
dependencies:
@ -14153,7 +14105,7 @@ packages:
dev: false
file:projects/tracker-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
resolution: {integrity: sha512-PKQD9e1Xy9uXITgztIDA3iXNVejZcOdX7dfkahRNSrPvWA7eiK1MOxO1TXGT8UKLAHCxOE9kaNQ9fJqaMca4og==, tarball: file:projects/tracker-resources.tgz}
resolution: {integrity: sha512-gNf+kxHBXh2EhUJNOFg36XUDijRQ7UkWIXl7Hf7Mt+Jg995unSk/8tKPsyJ+YcCkqGB7/Zfu4pTIvB/SGjEUwg==, tarball: file:projects/tracker-resources.tgz}
id: file:projects/tracker-resources.tgz
name: '@rush-temp/tracker-resources'
version: 0.0.0

View File

@ -49,6 +49,8 @@ import {
IssueStatusCategory,
Project,
ProjectStatus,
Sprint,
SprintStatus,
Team,
trackerId
} from '@anticrm/tracker'
@ -113,12 +115,25 @@ export function TypeProjectStatus (): Type<ProjectStatus> {
return { _class: tracker.class.TypeProjectStatus, label: 'TypeProjectStatus' as IntlString }
}
/**
* @public
*/
export function TypeSprintStatus (): Type<SprintStatus> {
return { _class: tracker.class.TypeSprintStatus, label: 'TypeSprintStatus' as IntlString }
}
/**
* @public
*/
@Model(tracker.class.TypeProjectStatus, core.class.Type, DOMAIN_MODEL)
export class TTypeProjectStatus extends TType {}
/**
* @public
*/
@Model(tracker.class.TypeSprintStatus, core.class.Type, DOMAIN_MODEL)
export class TTypeSprintStatus extends TType {}
/**
* @public
*/
@ -201,6 +216,9 @@ export class TIssue extends TAttachedDoc implements Issue {
@Prop(TypeString(), tracker.string.Rank)
@Hidden()
rank!: string
@Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint)
sprint!: Ref<Sprint> | null
}
/**
@ -269,6 +287,40 @@ export class TProject extends TDoc implements Project {
declare space: Ref<Team>
}
/**
* @public
*/
@Model(tracker.class.Sprint, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Sprint, tracker.icon.Sprint, tracker.string.Sprint)
export class TSprint extends TDoc implements Sprint {
@Prop(TypeString(), tracker.string.Title)
// @Index(IndexKind.FullText)
label!: string
@Prop(TypeMarkup(), tracker.string.Description)
description?: Markup
@Prop(TypeSprintStatus(), tracker.string.Status)
status!: SprintStatus
@Prop(TypeRef(contact.class.Employee), tracker.string.ProjectLead)
lead!: Ref<Employee> | null
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, undefined, attachment.string.Files)
attachments?: number
@Prop(TypeDate(false), tracker.string.StartDate)
startDate!: Timestamp
@Prop(TypeDate(false), tracker.string.TargetDate)
targetDate!: Timestamp
declare space: Ref<Team>
}
export function createModel (builder: Builder): void {
builder.createModel(
TTeam,
@ -277,7 +329,9 @@ export function createModel (builder: Builder): void {
TIssueStatus,
TIssueStatusCategory,
TTypeIssuePriority,
TTypeProjectStatus
TTypeProjectStatus,
TSprint,
TTypeSprintStatus
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
@ -298,6 +352,11 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.ProjectEditor,
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
},
{
key: '',
presenter: tracker.component.SprintEditor,
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
},
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } },
{
key: '$lookup.assignee',
@ -405,6 +464,7 @@ export function createModel (builder: Builder): void {
const backlogId = 'backlog'
const boardId = 'board'
const projectsId = 'projects'
const sprintsId = 'sprints'
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.IssuePresenter
@ -434,6 +494,10 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.ProjectTitlePresenter
})
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributePresenter, {
presenter: tracker.component.SprintTitlePresenter
})
builder.mixin(tracker.class.Issue, core.class.Class, setting.mixin.Editable, {})
builder.mixin(tracker.class.TypeProjectStatus, core.class.Class, view.mixin.AttributeEditor, {
@ -516,6 +580,12 @@ export function createModel (builder: Builder): void {
label: tracker.string.Projects,
icon: tracker.icon.Projects,
component: tracker.component.TeamProjects
},
{
id: sprintsId,
label: tracker.string.Sprints,
icon: tracker.icon.Sprint,
component: tracker.component.Sprints
}
]
}
@ -787,6 +857,34 @@ export function createModel (builder: Builder): void {
},
tracker.action.SetProject
)
createAction(
builder,
{
action: view.actionImpl.ValueSelector,
actionPopup: view.component.ValueSelector,
actionProps: {
attribute: 'sprint',
_class: tracker.class.Sprint,
query: {},
searchField: 'label',
placeholder: tracker.string.Sprint
},
label: tracker.string.Sprint,
icon: tracker.icon.Sprint,
keyBinding: [],
input: 'none',
category: tracker.category.Tracker,
target: tracker.class.Issue,
context: {
mode: ['context'],
application: tracker.app.Tracker,
group: 'edit'
}
},
tracker.action.SetSprint
)
createAction(
builder,
{

View File

@ -37,7 +37,8 @@ export default mergeIds(trackerId, tracker, {
},
component: {
// Required to pass build without errorsF
Nope: '' as AnyComponent
Nope: '' as AnyComponent,
SprintSelector: '' as AnyComponent
},
app: {
Tracker: '' as Ref<Application>

View File

@ -721,6 +721,7 @@ a.no-line {
.top-divider { border-top: 1px solid var(--divider-color); }
.bottom-divider { border-bottom: 1px solid var(--divider-color); }
.bottom-highlight-select { border-bottom: 1px solid var(--highlight-select); }
.left-divider { border-left: 1px solid var(--divider-color); }

View File

@ -26,7 +26,7 @@
import { resizeObserver } from '../resize'
interface ValueType {
id: number | string
id: number | string | null
icon?: Asset
iconColor?: string
label?: IntlString

View File

@ -13,13 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, afterUpdate } from 'svelte'
import { IntlString } from '@anticrm/platform'
import { afterUpdate, createEventDispatcher } from 'svelte'
import ui from '../../plugin'
import IconClose from '../icons/Close.svelte'
import ActionIcon from '../ActionIcon.svelte'
import Button from '../Button.svelte'
import Icon from '../Icon.svelte'
import IconClose from '../icons/Close.svelte'
import Label from '../Label.svelte'
import { daysInMonth } from './internal/DateUtils'
import MonthSquare from './MonthSquare.svelte'
@ -27,10 +26,11 @@
export let currentDate: Date | null
export let withTime: boolean = false
export let mondayStart: boolean = true
export let label = currentDate != null ? ui.string.EditDueDate : ui.string.AddDueDate
export let detail = ui.string.IssueNeedsToBeCompletedByThisDate
const dispatch = createEventDispatcher()
const popupCaption: IntlString = currentDate != null ? ui.string.EditDueDate : ui.string.AddDueDate
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
interface IEdits {
id: TEdits
@ -230,7 +230,7 @@
<div class="date-popup-container">
<div class="header">
<span class="fs-title overflow-label"><Label label={popupCaption} /></span>
<span class="fs-title overflow-label"><Label {label} /></span>
<ActionIcon
icon={IconClose}
size={'small'}
@ -241,9 +241,9 @@
</div>
<div class="content">
<div class="label">
<span class="bold"><Label label={ui.string.DueDate} /></span>
<span class="bold"><Label {label} /></span>
<span class="divider">-</span>
<Label label={ui.string.IssueNeedsToBeCompletedByThisDate} />
<Label label={detail} />
</div>
<div class="datetime-input">

View File

@ -36,6 +36,8 @@
export let shouldShowLabel: boolean = true
export let size: 'x-small' | 'small' = 'small'
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
export let label = ui.string.DueDate
export let detail = ui.string.IssueNeedsToBeCompletedByThisDate
const dispatch = createEventDispatcher()
@ -70,7 +72,7 @@
opened = true
showPopup(
DatePopup,
{ currentDate, mondayStart, withTime },
{ currentDate, mondayStart, withTime, label, detail },
undefined,
() => {
opened = false

View File

@ -157,4 +157,9 @@
<symbol id="copyBranch" viewBox="0 0 16 16">
<path d="M12.6229 11.528V5.66666C12.5954 4.93866 12.3116 4.29467 11.7624 3.744C11.2132 3.19333 10.5907 2.89467 9.87669 2.86667H8.9613V1L6.21514 3.8L8.9613 6.6V4.73333H9.87669C10.1238 4.752 10.3161 4.836 10.5083 5.02267C10.7005 5.20933 10.7829 5.41466 10.7921 5.66666V11.528C10.4436 11.7327 10.1712 12.049 10.0172 12.4278C9.86328 12.8067 9.83642 13.2268 9.94083 13.6228C10.0452 14.0188 10.2751 14.3685 10.5945 14.6176C10.914 14.8666 11.3053 15.0011 11.7075 15C12.1096 15.0011 12.5009 14.8666 12.8204 14.6176C13.1399 14.3685 13.3697 14.0188 13.4741 13.6228C13.5785 13.2268 13.5517 12.8067 13.3977 12.4278C13.2438 12.049 12.9714 11.7327 12.6229 11.528ZM11.7075 14.2533C11.1033 14.2533 10.609 13.74 10.609 13.1333C10.609 12.5267 11.1125 12.0133 11.7075 12.0133C12.3025 12.0133 12.8059 12.5267 12.8059 13.1333C12.8059 13.74 12.3025 14.2533 11.7075 14.2533ZM6.21514 3.8C6.21514 2.764 5.40045 1.93333 4.38436 1.93333C3.98219 1.93225 3.59093 2.0667 3.27144 2.31576C2.95195 2.56483 2.72213 2.91456 2.61772 3.31057C2.51332 3.70657 2.54018 4.12665 2.69412 4.50548C2.84807 4.88432 3.12047 5.20066 3.46898 5.40533V11.528C3.12047 11.7327 2.84807 12.049 2.69412 12.4278C2.54018 12.8067 2.51332 13.2268 2.61772 13.6228C2.72213 14.0188 2.95195 14.3685 3.27144 14.6176C3.59093 14.8666 3.98219 15.0011 4.38436 15C4.78654 15.0011 5.1778 14.8666 5.49729 14.6176C5.81678 14.3685 6.0466 14.0188 6.15101 13.6228C6.25541 13.2268 6.22855 12.8067 6.07461 12.4278C5.92066 12.049 5.64826 11.7327 5.29975 11.528V5.40533C5.83983 5.088 6.21514 4.49067 6.21514 3.8ZM5.48283 13.1333C5.48283 13.7493 4.97937 14.2533 4.38436 14.2533C3.78936 14.2533 3.2859 13.74 3.2859 13.1333C3.2859 12.5267 3.78936 12.0133 4.38436 12.0133C4.97937 12.0133 5.48283 12.5267 5.48283 13.1333ZM4.38436 4.92C3.78021 4.92 3.2859 4.40667 3.2859 3.8C3.2859 3.19333 3.78936 2.68 4.38436 2.68C4.97937 2.68 5.48283 3.19333 5.48283 3.8C5.48283 4.40667 4.97937 4.92 4.38436 4.92Z" />
</symbol>
<symbol id='sprint' viewBox="0 0 16 16" fill="none">
<path d="M5.99935 3.33337H9.33268M9.33268 3.33337H12.666M9.33268 3.33337V12.6667M5.99935 12.6667H9.33268M9.33268 12.6667H12.666" stroke="white"/>
<path d="M1.33398 10H0.833984V10.5H1.33398V10ZM1.33398 10.5H7.33398V9.5H1.33398V10.5ZM7.33398 5.5H3.33398V6.5H7.33398V5.5ZM0.833984 8V10H1.83398V8H0.833984ZM3.33398 5.5C1.95327 5.5 0.833984 6.61929 0.833984 8H1.83398C1.83398 7.17157 2.50556 6.5 3.33398 6.5V5.5Z" fill="white"/>
<path d="M14.666 6H15.166V5.5H14.666V6ZM14.666 5.5H11.3327V6.5H14.666V5.5ZM11.3327 10.5H12.666V9.5H11.3327V10.5ZM15.166 8V6H14.166V8H15.166ZM12.666 10.5C14.0467 10.5 15.166 9.38071 15.166 8H14.166C14.166 8.82843 13.4944 9.5 12.666 9.5V10.5Z" fill="white"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -183,7 +183,20 @@
"DurYears": "{years, plural, =0 {this year} =1 {a year} other {# years}}",
"StatusHistory": "State History",
"NewSubIssue": "Add sub-issue...",
"AddLabel": "Add label"
"AddLabel": "Add label",
"Sprint": "Sprint",
"NoSprint": "No Sprint",
"MoveToSprint": "Select Sprint",
"Sprints": "Sprints",
"AllSprints": "All",
"PlannedSprints": "Planned",
"ActiveSprints": "Active",
"ClosedSprints": "Done",
"AddToSprint": "Add to Sprint",
"NewSprint": "New Sprint",
"CreateSprint": "Create"
},
"status": {}
}

View File

@ -183,7 +183,20 @@
"DurYears": "{years, plural, =0 {меньше года} =1 {год} =2 {2 года} =3 {3 года} =4 {4 года} other {# лет}}",
"StatusHistory": "История состояний",
"NewSubIssue": "Добавить под-задачу...",
"AddLabel": "Добавить метку"
"AddLabel": "Добавить метку",
"Sprint": "Спринт",
"NoSprint": "Без Спринта",
"MoveToSprint": "Выбрать Спринт",
"Sprints": "Спринты",
"AllSprints": "Все",
"PlannedSprints": "Запланировано",
"ActiveSprints": "Активно",
"ClosedSprints": "Завершено",
"AddToSprint": "Добавить в Спринт",
"NewSprint": "Новый Спринт",
"CreateSprint": "Создать"
},
"status": {}
}

View File

@ -34,6 +34,7 @@ loadMetadata(tracker.icon, {
Labels: `${icons}#priority-nopriority`, // TODO: add icon
DueDate: `${icons}#inbox`, // TODO: add icon
Parent: `${icons}#myissues`, // TODO: add icon
Sprint: `${icons}#sprint`,
CategoryBacklog: `${icons}#status-backlog`,
CategoryUnstarted: `${icons}#status-todo`,
@ -57,6 +58,12 @@ loadMetadata(tracker.icon, {
ProjectStatusCompleted: `${icons}#project-status-completed`,
ProjectStatusCanceled: `${icons}#project-status-canceled`,
SprintStatusPlanned: `${icons}#project-status-planned`,
SprintStatusInProgress: `${icons}#project-status-in-progress`,
SprintStatusPaused: `${icons}#project-status-paused`,
SprintStatusCompleted: `${icons}#project-status-completed`,
SprintStatusCanceled: `${icons}#project-status-canceled`,
CopyID: `${icons}#copyID`,
CopyURL: `${icons}#copyURL`,
CopyBranch: `${icons}#copyBranch`

View File

@ -17,7 +17,7 @@
import { Employee } from '@anticrm/contact'
import core, { Account, AttachedData, Doc, generateId, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { Card, createQuery, getClient, KeyedAttribute, SpaceSelector } from '@anticrm/presentation'
import { calcRank, Issue, IssuePriority, IssueStatus, Project, Team } from '@anticrm/tracker'
import { calcRank, Issue, IssuePriority, IssueStatus, Project, Sprint, Team } from '@anticrm/tracker'
import tags, { TagElement, TagReference } from '@anticrm/tags'
import {
ActionIcon,
@ -40,12 +40,14 @@
import ProjectSelector from './ProjectSelector.svelte'
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SprintSelector from './sprints/SprintSelector.svelte'
export let space: Ref<Team>
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
export let sprint: Ref<Sprint> | null = null
let issueStatuses: WithLookup<IssueStatus>[] | undefined
export let parentIssue: Issue | undefined
@ -57,6 +59,7 @@
description: '',
assignee: assignee,
project: project,
sprint: sprint,
number: 0,
rank: '',
status: '' as Ref<IssueStatus>,
@ -220,6 +223,14 @@
object = { ...object, project: projectId }
}
const handleSprintIdChanged = (sprintId: Ref<Sprint> | null | undefined) => {
if (sprintId === undefined) {
return
}
object = { ...object, sprint: sprintId }
}
function addTagRef (tag: TagElement): void {
labels = [
...labels,
@ -326,6 +337,7 @@
}}
/>
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
<SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} />
{#if object.dueDate !== null}
<DatePresenter bind:value={object.dueDate} editable />
{/if}

View File

@ -38,11 +38,13 @@
const query = createQuery()
let rawProjects: Project[] = []
let loading = true
query.query(
tracker.class.Project,
{},
(res) => {
rawProjects = res
loading = false
},
{
sort: { modifiedOn: SortingOrder.Ascending }
@ -98,5 +100,6 @@
label={onlyIcon || projectText === undefined ? undefined : getEmbeddedLabel(projectText)}
icon={projectIcon}
disabled={!isEditable}
{loading}
on:click={handleProjectEditorOpened}
/>

View File

@ -9,6 +9,7 @@
export let viewlets: WithLookup<Viewlet>[] = []
export let label: string
export let search: string
export let showLabelSelector = false
$: viewslist = viewlets.map((views) => {
return {
@ -21,8 +22,12 @@
<div class="ac-header full">
<div class="ac-header__wrap-title">
<div class="ac-header__icon"><Icon icon={tracker.icon.Issues} size={'small'} /></div>
<span class="ac-header__title">{label}</span>
{#if showLabelSelector}
<slot name="label_selector" />
{:else}
<div class="ac-header__icon"><Icon icon={tracker.icon.Issues} size={'small'} /></div>
<span class="ac-header__title">{label}</span>
{/if}
<div class="ml-4"><FilterButton _class={tracker.class.Issue} /></div>
</div>
<SearchEdit bind:value={search} on:change={() => {}} />

View File

@ -66,7 +66,10 @@
$: if (docWidth > 900 && docSize) docSize = false
</script>
<IssuesHeader {viewlets} {label} bind:viewlet bind:search>
<IssuesHeader {viewlets} {label} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
<svelte:fragment slot="label_selector">
<slot name="label_selector" />
</svelte:fragment>
<svelte:fragment slot="extra">
{#if viewlet}
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} />
@ -91,7 +94,7 @@
<IssuesContent {viewlet} query={resultQuery} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside" class:float={asideFloat} class:shown={asideShown}>
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
<slot name="aside" />
</div>
{/if}

View File

@ -14,17 +14,19 @@
-->
<script lang="ts">
import { WithLookup } from '@anticrm/core'
import type { Issue, IssueStatus } from '@anticrm/tracker'
import { createQuery } from '@anticrm/presentation'
import { Component, Label } from '@anticrm/ui'
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@anticrm/presentation'
import tags from '@anticrm/tags'
import type { Issue, IssueStatus } from '@anticrm/tracker'
import { Component, Label } from '@anticrm/ui'
import { getFiltredKeys, isCollectionAttr } from '@anticrm/view-resources/src/utils'
import tracker from '../../../plugin'
import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import ProjectEditor from '../../projects/ProjectEditor.svelte'
import SprintEditor from '../../sprints/SprintEditor.svelte'
import AssigneeEditor from '../AssigneeEditor.svelte'
import DueDateEditor from '../DueDateEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import RelationEditor from '../RelationEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
export let issue: Issue
export let issueStatuses: WithLookup<IssueStatus>[]
@ -34,12 +36,25 @@
query.query(tracker.class.Issue, { blockedBy: issue._id }, (result) => {
showIsBlocking = result.length > 0
})
const client = getClient()
const hierarchy = client.getHierarchy()
let keys: KeyedAttribute[] = []
function updateKeys (ignoreKeys: string[]): void {
const filtredKeys = getFiltredKeys(hierarchy, issue._class, ignoreKeys)
keys = filtredKeys.filter((key) => !isCollectionAttr(hierarchy, key))
}
$: updateKeys(['title', 'description', 'priority', 'status', 'number', 'assignee', 'project', 'dueDate', 'sprint'])
</script>
<div class="content">
<span class="label">
<Label label={tracker.string.Status} />
</span>
<StatusEditor value={issue} statuses={issueStatuses} shouldShowLabel />
{#if issue.blockedBy?.length}
@ -83,6 +98,13 @@
</span>
<ProjectEditor value={issue} />
{#if issue.sprint}
<span class="label">
<Label label={tracker.string.Sprint} />
</span>
<SprintEditor value={issue} />
{/if}
{#if issue.dueDate !== null}
<div class="divider" />
@ -91,6 +113,13 @@
</span>
<DueDateEditor value={issue} />
{/if}
{#if keys.length > 0}
<div class="divider" />
{#each keys as key (typeof key === 'string' ? key : key.key)}
<AttributeBarEditor {key} _class={issue._class} object={issue} showHeader={true} />
{/each}
{/if}
</div>
<style lang="scss">

View File

@ -19,22 +19,25 @@
import { Panel } from '@anticrm/panel'
import { getResource } from '@anticrm/platform'
import presentation, { createQuery, getClient, MessageViewer } from '@anticrm/presentation'
import setting, { settingId } from '@anticrm/setting'
import type { Issue, IssueStatus, Team } from '@anticrm/tracker'
import {
Button,
EditBox,
getCurrentLocation,
IconAttachment,
IconEdit,
IconMoreH,
Label,
navigate,
Scroller,
showPopup,
Spinner
} from '@anticrm/ui'
import { ContextMenu, UpDownNavigator } from '@anticrm/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import tracker from '../../../plugin'
import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import IssueStatusActivity from '../IssueStatusActivity.svelte'
import ControlPanel from './ControlPanel.svelte'
import CopyToClipboard from './CopyToClipboard.svelte'
@ -283,9 +286,26 @@
{#if issueId}{issueId}{/if}
</span>
<svelte:fragment slot="actions">
<div class="flex-grow" />
{#if issueId}
<CopyToClipboard issueUrl={generateIssueShortLink(issueId)} {issueId} />
{/if}
<Button
icon={setting.icon.Setting}
kind={'transparent'}
showTooltip={{ label: setting.string.ClassSetting }}
on:click={(ev) => {
ev.stopPropagation()
const loc = getCurrentLocation()
loc.path[2] = settingId
loc.path[3] = 'setting'
loc.path[4] = 'classes'
loc.path.length = 5
loc.query = { _class }
loc.fragment = undefined
navigate(loc)
}}
/>
</svelte:fragment>
<svelte:fragment slot="custom-attributes">

View File

@ -2,10 +2,11 @@
import { getClient } from '@anticrm/presentation'
import { StyledTextBox } from '@anticrm/text-editor'
import { Project } from '@anticrm/tracker'
import { EditBox } from '@anticrm/ui'
import { Button, EditBox, Icon, showPopup } from '@anticrm/ui'
import { DocAttributeBar } from '@anticrm/view-resources'
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
import ProjectPopup from './ProjectPopup.svelte'
export let project: Project
@ -14,11 +15,26 @@
async function change (field: string, value: any) {
await client.update(project, { [field]: value })
}
function selectProject (evt: MouseEvent): void {
showPopup(ProjectPopup, { _class: tracker.class.Project }, evt.target as HTMLElement, (value) => {
if (value != null) {
project = value
}
})
}
</script>
<IssuesView query={{ project: project._id, space: project.space }} label={project.label}>
<svelte:fragment slot="label_selector">
<Button size={'small'} kind={'link'} on:click={selectProject}>
<svelte:fragment slot="content">
<div class="ac-header__icon"><Icon icon={tracker.icon.Issues} size={'small'} /></div>
<span class="ac-header__title">{project.label}</span>
</svelte:fragment>
</Button>
</svelte:fragment>
<svelte:fragment slot="aside">
<div class="flex-row p-4">
<div class="flex-row p-4 w-60 left-divider">
<div class="fs-title text-xl">
<EditBox bind:value={project.label} maxWidth="39rem" on:change={() => change('label', project.label)} />
</div>

View File

@ -18,7 +18,7 @@
import { IntlString } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation'
import { Project } from '@anticrm/tracker'
import { Button, IconAdd, IconOptions, Label, showPopup } from '@anticrm/ui'
import { Button, IconAdd, Label, showPopup } from '@anticrm/ui'
import tracker from '../../plugin'
import { getIncludedProjectStatuses, projectsTitleMap, ProjectsViewMode } from '../../utils'
import NewProject from './NewProject.svelte'
@ -124,7 +124,7 @@
/>
</div>
</div>
<div class="ml-3 filterButton">
<!-- <div class="ml-3 filterButton">
<Button
size="small"
icon={IconAdd}
@ -133,9 +133,9 @@
label={tracker.string.Filter}
on:click={() => {}}
/>
</div>
</div> -->
</div>
<div class="flex-center">
<!-- <div class="flex-center">
<div class="flex-center">
<div class="buttonWrapper">
<Button selected size="small" shape="rectangle-right" icon={tracker.icon.ProjectsList} />
@ -147,7 +147,7 @@
<div class="ml-3">
<Button size="small" icon={IconOptions} />
</div>
</div>
</div> -->
</div>
<ProjectsListBrowser
_class={tracker.class.Project}

View File

@ -0,0 +1,51 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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, DocumentQuery, Ref } from '@anticrm/core'
import { ObjectCreate, ObjectPopup } from '@anticrm/presentation'
import { Project } from '@anticrm/tracker'
import ProjectTitlePresenter from './ProjectTitlePresenter.svelte'
export let _class: Ref<Class<Project>>
export let selected: Ref<Project> | undefined
export let sprintQuery: DocumentQuery<Project> = {}
export let create: ObjectCreate | undefined = undefined
export let allowDeselect = false
$: _create =
create !== undefined
? {
...create,
update: (doc: Doc) => (doc as Project).label
}
: undefined
</script>
<ObjectPopup
{_class}
{selected}
bind:docQuery={sprintQuery}
searchField={'label'}
multiSelect={false}
{allowDeselect}
shadows={true}
create={_create}
on:update
on:close
>
<svelte:fragment slot="item" let:item={sprint}>
<ProjectTitlePresenter value={sprint} />
</svelte:fragment>
</ObjectPopup>

View File

@ -27,16 +27,12 @@
</script>
{#if value}
<div class="flex-presenter projectPresenterRoot" on:click={navigateToProject}>
<span title={value.label} class="projectLabel">{value.label}</span>
<div class="flex-presenter flex-grow" on:click={navigateToProject}>
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
</div>
{/if}
<style lang="scss">
.projectPresenterRoot {
max-width: 5rem;
}
.projectLabel {
display: block;
min-width: 0;

View File

@ -14,12 +14,17 @@
-->
<script lang="ts">
import { Project } from '@anticrm/tracker'
import { Icon } from '@anticrm/ui'
import tracker from '../../plugin'
export let value: Project | undefined
</script>
{#if value}
<span class="overflow-label">
{value.label}
</span>
<span class="overflow-label flex">
<Icon icon={value.icon ?? tracker.icon.Project} size={'small'} />
<div class="ml-2 mr-2">
{value.label}
</div></span
>
{/if}

View File

@ -135,7 +135,7 @@
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="projectPresenter">
<div class="projectPresenter flex-grow">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { getClient } from '@anticrm/presentation'
import { StyledTextBox } from '@anticrm/text-editor'
import { Sprint } from '@anticrm/tracker'
import { Button, EditBox, Icon, showPopup } from '@anticrm/ui'
import { DocAttributeBar } from '@anticrm/view-resources'
import tracker from '../../plugin'
import Expanded from '../icons/Expanded.svelte'
import IssuesView from '../issues/IssuesView.svelte'
import SprintPopup from './SprintPopup.svelte'
export let sprint: Sprint
const client = getClient()
async function change (field: string, value: any) {
await client.update(sprint, { [field]: value })
}
let container: HTMLElement
function selectSprint (evt: MouseEvent): void {
showPopup(SprintPopup, { _class: tracker.class.Sprint }, container, (value) => {
if (value != null) {
sprint = value
}
})
}
</script>
<IssuesView query={{ sprint: sprint._id, space: sprint.space }} label={sprint.label}>
<svelte:fragment slot="label_selector">
<div bind:this={container}>
<Button size={'small'} kind={'link'} on:click={selectSprint}>
<svelte:fragment slot="content">
<div class="ac-header__icon"><Icon icon={tracker.icon.Issues} size={'small'} /></div>
<span class="ac-header__title mr-1">{sprint.label}</span>
<Icon icon={Expanded} size={'small'} />
</svelte:fragment>
</Button>
</div>
</svelte:fragment>
<svelte:fragment slot="aside">
<div class="flex-grow p-4 w-60 left-divider">
<div class="fs-title text-xl">
<EditBox bind:value={sprint.label} maxWidth="39rem" on:change={() => change('label', sprint.label)} />
</div>
<div class="mt-2">
<StyledTextBox
alwaysEdit={true}
showButtons={false}
placeholder={tracker.string.Description}
content={sprint.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<DocAttributeBar object={sprint} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
</div>
</svelte:fragment>
</IssuesView>

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 { Data, Ref } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { Card, EmployeeBox, getClient, SpaceSelector } from '@anticrm/presentation'
import { Sprint, SprintStatus, Team } from '@anticrm/tracker'
import ui, { DatePresenter, EditBox } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import SprintStatusSelector from './SprintStatusSelector.svelte'
export let space: Ref<Team>
const dispatch = createEventDispatcher()
const client = getClient()
const object: Data<Sprint> = {
label: '' as IntlString,
description: '',
status: SprintStatus.Planned,
lead: null,
comments: 0,
attachments: 0,
startDate: Date.now(),
targetDate: Date.now() + 14 * 24 * 60 * 60 * 1000
}
async function onSave () {
await client.createDoc(tracker.class.Sprint, space, object)
}
const handleProjectStatusChanged = (newSprintStatus: SprintStatus | undefined) => {
if (newSprintStatus === undefined) {
return
}
object.status = newSprintStatus
}
</script>
<Card
label={tracker.string.NewSprint}
okAction={onSave}
canSave={object.label !== ''}
okLabel={tracker.string.CreateSprint}
on:close={() => dispatch('close')}
>
<svelte:fragment slot="header">
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space />
</svelte:fragment>
<div class="label">
<EditBox
bind:value={object.label}
placeholder={tracker.string.ProjectNamePlaceholder}
maxWidth="37.5rem"
kind="large-style"
focus
/>
</div>
<div class="description">
<EditBox
bind:value={object.description}
placeholder={tracker.string.ProjectDescriptionPlaceholder}
maxWidth="37.5rem"
kind="editbox"
/>
</div>
<div slot="pool" class="flex-row-center text-sm gap-1-5">
<SprintStatusSelector selectedSprintStatus={object.status} onSprintStatusChange={handleProjectStatusChanged} />
<EmployeeBox
label={tracker.string.ProjectLead}
placeholder={tracker.string.AssignTo}
bind:value={object.lead}
allowDeselect
titleDeselect={tracker.string.Unassigned}
/>
<DatePresenter
bind:value={object.startDate}
editable
label={tracker.string.StartDate}
detail={ui.string.SelectDate}
/>
<DatePresenter
bind:value={object.targetDate}
editable
label={tracker.string.TargetDate}
detail={ui.string.SelectDate}
/>
</div>
</Card>
<style>
.label {
margin-bottom: 10px;
}
.description {
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,206 @@
<!--
// 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 { DocumentQuery, FindOptions, SortingOrder } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation'
import { Sprint } from '@anticrm/tracker'
import { Button, Icon, IconAdd, Label, showPopup } from '@anticrm/ui'
import tracker from '../../plugin'
import { getIncludedSprintStatuses, sprintTitleMap, SprintViewMode } from '../../utils'
import NewSprint from './NewSprint.svelte'
import SprintDatePresenter from './SprintDatePresenter.svelte'
import SprintListBrowser from './SprintListBrowser.svelte'
export let label: IntlString
export let query: DocumentQuery<Sprint> = {}
export let search: string = ''
export let mode: SprintViewMode = 'all'
const ENTRIES_LIMIT = 200
const resultSprintsQuery = createQuery()
const sprintOptions: FindOptions<Sprint> = {
sort: { modifiedOn: SortingOrder.Descending },
limit: ENTRIES_LIMIT,
lookup: { lead: contact.class.Employee }
}
let resultSprints: Sprint[] = []
$: includedSprintStatuses = getIncludedSprintStatuses(mode)
$: title = sprintTitleMap[mode]
$: includedSprintsQuery = { status: { $in: includedSprintStatuses } }
$: baseQuery = {
...includedSprintsQuery,
...query
}
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
$: resultSprintsQuery.query<Sprint>(
tracker.class.Sprint,
{ ...resultQuery },
(result) => {
resultSprints = result
},
sprintOptions
)
const space = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
const showCreateDialog = async () => {
showPopup(NewSprint, { space, targetElement: null }, null)
}
const handleViewModeChanged = (newMode: SprintViewMode) => {
if (newMode === undefined || newMode === mode) {
return
}
mode = newMode
}
</script>
<div>
<div class="fs-title flex-between header">
<div class="flex-center">
<Label {label} />
<div class="projectTitle">
<Label label={title} />
</div>
</div>
<Button size="small" icon={IconAdd} label={tracker.string.Sprint} kind="secondary" on:click={showCreateDialog} />
</div>
<div class="itemsContainer">
<div class="flex-center">
<div class="flex-center">
<div class="buttonWrapper">
<Button
size="small"
shape="rectangle-right"
selected={mode === 'all'}
label={tracker.string.AllSprints}
on:click={() => handleViewModeChanged('all')}
/>
</div>
<div class="buttonWrapper">
<Button
size="small"
shape="rectangle"
selected={mode === 'planned'}
label={tracker.string.PlannedSprints}
on:click={() => handleViewModeChanged('planned')}
/>
</div>
<div class="buttonWrapper">
<Button
size="small"
shape="rectangle"
selected={mode === 'active'}
label={tracker.string.ActiveSprints}
on:click={() => handleViewModeChanged('active')}
/>
</div>
<div class="buttonWrapper">
<Button
size="small"
shape="rectangle-left"
selected={mode === 'closed'}
label={tracker.string.ClosedSprints}
on:click={() => handleViewModeChanged('closed')}
/>
</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} />
</div>
</div> -->
</div>
<SprintListBrowser
_class={tracker.class.Sprint}
itemsConfig={[
{ key: '', presenter: Icon, props: { icon: tracker.icon.Sprint, size: 'small' } },
{ key: '', presenter: tracker.component.SprintPresenter, props: { kind: 'list' } },
{
key: '$lookup.lead',
presenter: tracker.component.LeadPresenter,
props: { defaultClass: contact.class.Employee, shouldShowLabel: false }
},
{ key: '', presenter: SprintDatePresenter, props: { field: 'startDate' } },
{ key: '', presenter: SprintDatePresenter, props: { field: 'targetDate' } },
{ key: '', presenter: tracker.component.SprintStatusPresenter }
]}
sprints={resultSprints}
/>
</div>
<style lang="scss">
.header {
min-height: 3.5rem;
padding-left: 2.25rem;
padding-right: 1.35rem;
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;
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);
}
.buttonWrapper {
margin-right: 1px;
&:last-child {
margin-right: 0;
}
}
.filterButton {
color: var(--caption-color);
}
</style>

View File

@ -0,0 +1,48 @@
<!--
// 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 { getClient } from '@anticrm/presentation'
import { Sprint } from '@anticrm/tracker'
import { DatePresenter } from '@anticrm/ui'
export let value: Sprint
export let field = 'targetDate'
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
const client = getClient()
$: dateMs = (value as any)[field] as number
const handleDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail
if (newDate === undefined || dateMs === newDate) {
return
}
await client.update(value, { [field]: newDate })
}
</script>
<DatePresenter
value={dateMs}
editable={true}
shouldShowLabel={true}
icon={'normal'}
{kind}
on:change={handleDateChanged}
/>

View File

@ -0,0 +1,74 @@
<!--
// 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, Sprint } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import { ButtonKind, ButtonShape, ButtonSize, tooltip } from '@anticrm/ui'
import { IntlString } from '@anticrm/platform'
import tracker from '../../plugin'
import SprintSelector from './SprintSelector.svelte'
export let value: Issue
export let isEditable: boolean = true
export let shouldShowLabel: boolean = true
export let popupPlaceholder: IntlString = tracker.string.MoveToSprint
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%'
export let onlyIcon: boolean = false
const client = getClient()
const handleSprintIdChanged = async (newSprintId: Ref<Sprint> | null | undefined) => {
if (!isEditable || newSprintId === undefined || value.sprint === newSprintId) {
return
}
await client.updateCollection(
value._class,
value.space,
value._id,
value.attachedTo,
value.attachedToClass,
value.collection,
{ sprint: newSprintId }
)
}
</script>
{#if value.sprint || shouldShowPlaceholder}
<div
class="clear-mins"
use:tooltip={{ label: value.sprint ? tracker.string.MoveToSprint : tracker.string.AddToSprint }}
>
<SprintSelector
{kind}
{size}
{shape}
{width}
{justify}
{isEditable}
{shouldShowLabel}
{popupPlaceholder}
{onlyIcon}
value={value.sprint}
onSprintIdChange={handleSprintIdChanged}
/>
</div>
{/if}

View File

@ -0,0 +1,241 @@
<!--
// 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, Sprint } from '@anticrm/tracker'
import { CheckBox, Spinner, tooltip } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import { buildModel, 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 sprints: Sprint[] | 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
}
}
$: options = { ...baseOptions } as FindOptions<Sprint>
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
$: objectRefs.length = sprints?.length ?? 0
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 (!sprints) {
return
}
let position =
(docObject !== undefined ? sprints?.findIndex((x) => x._id === docObject?._id) : selectedRowIndex) ?? -1
position += offset
if (position < 0) {
position = 0
}
if (position >= sprints.length) {
position = sprints.length - 1
}
const objectRef = objectRefs[position]
selectedRowIndex = position
handleRowFocused(sprints[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 sprints}
{#each sprints as docObject (docObject._id)}
<div
bind:this={objectRefs[sprints.findIndex((x) => x === docObject)]}
class="listGrid"
class:mListGridChecked={selectedObjectIdsSet.has(docObject._id)}
class:mListGridFixed={selectedRowIndex === sprints.findIndex((x) => x === docObject)}
class:mListGridSelected={selectedRowIndex === sprints.findIndex((x) => x === docObject)}
on:focus={() => {}}
on:mouseover={() => handleRowFocused(docObject)}
>
<div class="contentWrapper">
{#each itemModels as attributeModel, attributeModelIndex}
{#if attributeModelIndex === 0}
<div class="gridElement">
<div
class="eListGridCheckBox"
use:tooltip={{ direction: 'bottom', label: tracker.string.SelectIssue }}
>
<CheckBox
checked={selectedObjectIdsSet.has(docObject._id)}
on:value={(event) => {
onObjectChecked([docObject], event.detail)
}}
/>
</div>
<div class="iconPresenter">
<svelte:component
this={attributeModel.presenter}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
{...attributeModel.props}
/>
</div>
</div>
{:else if attributeModelIndex === 1}
<div class="projectPresenter flex-grow">
<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) ?? ''}
projectId={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 { Sprint } from '@anticrm/tracker'
import { BuildModelKey } from '@anticrm/view'
import {
ActionContext,
focusStore,
ListSelectionProvider,
LoadingProps,
SelectDirection,
selectionStore
} from '@anticrm/view-resources'
import { onMount } from 'svelte'
import SprintList from './SprintList.svelte'
export let _class: Ref<Class<Doc>>
export let itemsConfig: (BuildModelKey | string)[]
export let loadingProps: LoadingProps | undefined = undefined
export let sprints: Sprint[] = []
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
sprintsList.onElementSelected(offset, of)
}
})
let sprintsList: SprintList
$: if (sprintsList !== undefined) {
listProvider.update(sprints)
}
onMount(() => {
;(document.activeElement as HTMLElement)?.blur()
})
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<SprintList
bind:this={sprintsList}
{_class}
{itemsConfig}
{loadingProps}
{sprints}
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,51 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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, DocumentQuery, Ref } from '@anticrm/core'
import { ObjectCreate, ObjectPopup } from '@anticrm/presentation'
import { Sprint } from '@anticrm/tracker'
import SprintTitlePresenter from './SprintTitlePresenter.svelte'
export let _class: Ref<Class<Sprint>>
export let selected: Ref<Sprint> | undefined
export let sprintQuery: DocumentQuery<Sprint> = {}
export let create: ObjectCreate | undefined = undefined
export let allowDeselect = false
$: _create =
create !== undefined
? {
...create,
update: (doc: Doc) => (doc as Sprint).label
}
: undefined
</script>
<ObjectPopup
{_class}
{selected}
bind:docQuery={sprintQuery}
searchField={'label'}
multiSelect={false}
{allowDeselect}
shadows={true}
create={_create}
on:update
on:close
>
<svelte:fragment slot="item" let:item={sprint}>
<SprintTitlePresenter value={sprint} />
</svelte:fragment>
</ObjectPopup>

View File

@ -0,0 +1,46 @@
<!--
// 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 { Sprint } from '@anticrm/tracker'
import { getCurrentLocation, navigate } from '@anticrm/ui'
export let value: WithLookup<Sprint>
function navigateToSprint () {
const loc = getCurrentLocation()
loc.path[5] = value._id
loc.path.length = 6
navigate(loc)
}
</script>
{#if value}
<div class="flex-presenter flex-grow" on:click={navigateToSprint}>
<span title={value.label} class="projectLabel flex-grow">{value.label}</span>
</div>
{/if}
<style lang="scss">
.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,102 @@
<!--
// 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 { getEmbeddedLabel, IntlString, translate } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation'
import { Sprint } from '@anticrm/tracker'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import { Button, ButtonShape, eventToHTMLElement, SelectPopup, showPopup } from '@anticrm/ui'
import tracker from '../../plugin'
export let value: Ref<Sprint> | null | undefined
export let shouldShowLabel: boolean = true
export let isEditable: boolean = true
export let onSprintIdChange: ((newSprintId: Ref<Sprint> | undefined) => void) | undefined = undefined
export let popupPlaceholder: IntlString = tracker.string.AddToSprint
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'
export let onlyIcon: boolean = false
let selectedSprint: Sprint | undefined
let defaultSprintLabel = ''
const query = createQuery()
let rawSprints: Sprint[] = []
query.query(
tracker.class.Sprint,
{},
(res) => {
rawSprints = res
},
{
sort: { modifiedOn: SortingOrder.Ascending }
}
)
$: handleSelectedSprintIdUpdated(value, rawSprints)
$: translate(tracker.string.Sprint, {}).then((result) => (defaultSprintLabel = result))
const sprintIcon = tracker.icon.Sprint
$: sprintText = shouldShowLabel ? selectedSprint?.label ?? defaultSprintLabel : undefined
const handleSelectedSprintIdUpdated = async (newSprintId: Ref<Sprint> | null | undefined, sprints: Sprint[]) => {
if (newSprintId === null || newSprintId === undefined) {
selectedSprint = undefined
return
}
selectedSprint = sprints.find((it) => it._id === newSprintId)
}
const handleSprintEditorOpened = async (event: MouseEvent): Promise<void> => {
event.stopPropagation()
if (!isEditable) {
return
}
const sprintInfo = [
{ id: null, icon: tracker.icon.Sprint, label: tracker.string.NoSprint },
...rawSprints.map((p) => ({
id: p._id,
icon: tracker.icon.Sprint,
text: p.label
}))
]
showPopup(
SelectPopup,
{ value: sprintInfo, placeholder: popupPlaceholder, searchable: true },
eventToHTMLElement(event),
onSprintIdChange
)
}
</script>
<Button
{kind}
{size}
{shape}
{width}
{justify}
label={onlyIcon || sprintText === undefined ? undefined : getEmbeddedLabel(sprintText)}
icon={sprintIcon}
disabled={!isEditable}
on:click={handleSprintEditorOpened}
/>

View File

@ -0,0 +1,54 @@
<!--
// 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 { getClient } from '@anticrm/presentation'
import { Sprint, SprintStatus } from '@anticrm/tracker'
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import tracker from '../../plugin'
import SprintStatusSelector from './SprintStatusSelector.svelte'
export let value: Sprint
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: SprintStatus | undefined) => {
if (!isEditable || newStatus === undefined || value.status === newStatus) {
return
}
await client.update(value, { status: newStatus })
}
</script>
{#if value}
<SprintStatusSelector
{kind}
{size}
{width}
{justify}
{isEditable}
{shouldShowLabel}
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
selectedSprintStatus={value.status}
onSprintStatusChange={handleProjectStatusChanged}
/>
{/if}

View File

@ -0,0 +1,68 @@
<!--
// 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 { SprintStatus } from '@anticrm/tracker'
import { Button, showPopup, SelectPopup, eventToHTMLElement } from '@anticrm/ui'
import type { ButtonKind, ButtonSize, LabelAndProps } from '@anticrm/ui'
import tracker from '../../plugin'
import { defaultSprintStatuses, sprintStatusAssets } from '../../utils'
export let selectedSprintStatus: SprintStatus | undefined
export let shouldShowLabel: boolean = true
export let onSprintStatusChange: ((newSprintStatus: SprintStatus | undefined) => void) | undefined = undefined
export let isEditable: boolean = true
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
export let showTooltip: LabelAndProps | undefined = undefined
$: selectedStatusIcon = selectedSprintStatus
? sprintStatusAssets[selectedSprintStatus].icon
: tracker.icon.SprintStatusPlanned
$: selectedStatusLabel = shouldShowLabel
? selectedSprintStatus
? sprintStatusAssets[selectedSprintStatus].label
: tracker.string.Planned
: undefined
$: statusesInfo = defaultSprintStatuses.map((s) => ({ id: s, ...sprintStatusAssets[s] }))
const handleSprintStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
eventToHTMLElement(event),
onSprintStatusChange
)
}
</script>
<Button
{kind}
{size}
{width}
{justify}
disabled={!isEditable}
icon={selectedStatusIcon}
label={selectedStatusLabel}
{showTooltip}
on:click={handleSprintStatusEditorOpened}
/>

View File

@ -0,0 +1,36 @@
<!--
// 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 { Sprint } from '@anticrm/tracker'
import { getMonthName, Icon } from '@anticrm/ui'
import tracker from '../../plugin'
export let value: Sprint | undefined
</script>
{#if value}
<span class="overflow-label flex-row-center flex-grow">
<Icon icon={tracker.icon.Sprint} size={'small'} />
<div class="ml-2 mr-2">
{value.label}
</div>
<span class="flex flex-grow justify-end">
{new Date(value.startDate).getDate()}
{getMonthName(new Date(value.startDate), 'short')}
-
{new Date(value.targetDate).getDate()}
{getMonthName(new Date(value.targetDate), 'short')}
</span>
</span>
{/if}

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 { IntlString } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation'
import { Sprint, Team } from '@anticrm/tracker'
import { closePopup, closeTooltip, location } from '@anticrm/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import { SprintViewMode } from '../../utils'
import EditSprint from './EditSprint.svelte'
import SprintBrowser from './SprintBrowser.svelte'
export let currentSpace: Ref<Team>
export let label: IntlString = tracker.string.Sprints
export let search: string = ''
export let mode: SprintViewMode = 'all'
let sprintId: Ref<Sprint> | undefined
let sprint: Sprint | undefined
onDestroy(
location.subscribe(async (loc) => {
closeTooltip()
closePopup()
sprintId = loc.path[5] as Ref<Sprint>
})
)
const sprintQuery = createQuery()
$: if (sprintId !== undefined) {
sprintQuery.query(tracker.class.Sprint, { _id: sprintId }, (result) => {
sprint = result.shift()
})
} else {
sprintQuery.unsubscribe()
sprint = undefined
}
</script>
{#if sprint}
<EditSprint {sprint} />
{:else}
<SprintBrowser {label} query={{ space: currentSpace }} {search} {mode} />
{/if}

View File

@ -62,6 +62,13 @@ import { copyToClipboard, getIssueId, getIssueTitle, resolveLocation } from './i
import CreateIssue from './components/CreateIssue.svelte'
import RelationsPopup from './components/RelationsPopup.svelte'
import Sprints from './components/sprints/Sprints.svelte'
import SprintPresenter from './components/sprints/SprintPresenter.svelte'
import SprintStatusPresenter from './components/sprints/SprintStatusPresenter.svelte'
import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svelte'
import SprintSelector from './components/sprints/SprintSelector.svelte'
import SprintEditor from './components/sprints/SprintEditor.svelte'
export async function queryIssue<D extends Issue> (
_class: Ref<Class<D>>,
client: Client,
@ -157,7 +164,13 @@ export default async (): Promise<Resources> => ({
Roadmap,
IssuePreview,
RelationsPopup,
CreateIssue
CreateIssue,
Sprints,
SprintPresenter,
SprintStatusPresenter,
SprintTitlePresenter,
SprintSelector,
SprintEditor
},
completion: {
IssueQuery: async (client: Client, query: string) => await queryIssue(tracker.class.Issue, client, query)

View File

@ -200,7 +200,20 @@ export default mergeIds(trackerId, tracker, {
DurMonths: '' as IntlString,
DurYears: '' as IntlString,
StatusHistory: '' as IntlString,
AddLabel: '' as IntlString
AddLabel: '' as IntlString,
Sprint: '' as IntlString,
Sprints: '' as IntlString,
AllSprints: '' as IntlString,
PlannedSprints: '' as IntlString,
ActiveSprints: '' as IntlString,
ClosedSprints: '' as IntlString,
NewSprint: '' as IntlString,
CreateSprint: '' as IntlString,
AddToSprint: '' as IntlString,
MoveToSprint: '' as IntlString,
NoSprint: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,
@ -219,6 +232,7 @@ export default mergeIds(trackerId, tracker, {
PriorityPresenter: '' as AnyComponent,
PriorityEditor: '' as AnyComponent,
ProjectEditor: '' as AnyComponent,
SprintEditor: '' as AnyComponent,
StatusPresenter: '' as AnyComponent,
StatusEditor: '' as AnyComponent,
AssigneePresenter: '' as AnyComponent,
@ -242,7 +256,12 @@ export default mergeIds(trackerId, tracker, {
TeamProjects: '' as AnyComponent,
IssuePreview: '' as AnyComponent,
RelationsPopup: '' as AnyComponent,
CreateIssue: '' as AnyComponent
CreateIssue: '' as AnyComponent,
Sprints: '' as AnyComponent,
SprintPresenter: '' as AnyComponent,
SprintStatusPresenter: '' as AnyComponent,
SprintTitlePresenter: '' as AnyComponent
},
function: {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>

View File

@ -21,7 +21,8 @@ import {
IssuesDateModificationPeriod,
IssuesGrouping,
IssuesOrdering,
ProjectStatus
ProjectStatus,
SprintStatus
} from '@anticrm/tracker'
import tracker from './plugin'
@ -63,6 +64,13 @@ export const defaultProjectStatuses = [
ProjectStatus.Canceled
]
export const defaultSprintStatuses = [
SprintStatus.Planned,
SprintStatus.InProgress,
SprintStatus.Completed,
SprintStatus.Canceled
]
export const projectStatusAssets: Record<ProjectStatus, { icon: Asset, label: IntlString }> = {
[ProjectStatus.Backlog]: { icon: tracker.icon.ProjectStatusBacklog, label: tracker.string.Backlog },
[ProjectStatus.Planned]: { icon: tracker.icon.ProjectStatusPlanned, label: tracker.string.Planned },
@ -71,6 +79,14 @@ export const projectStatusAssets: Record<ProjectStatus, { icon: Asset, label: In
[ProjectStatus.Completed]: { icon: tracker.icon.ProjectStatusCompleted, label: tracker.string.Completed },
[ProjectStatus.Canceled]: { icon: tracker.icon.ProjectStatusCanceled, label: tracker.string.Canceled }
}
export const sprintStatusAssets: Record<SprintStatus, { icon: Asset, label: IntlString }> = {
[SprintStatus.Planned]: { icon: tracker.icon.SprintStatusPlanned, label: tracker.string.Planned },
[SprintStatus.InProgress]: { icon: tracker.icon.SprintStatusInProgress, label: tracker.string.InProgress },
[SprintStatus.Completed]: { icon: tracker.icon.SprintStatusCompleted, label: tracker.string.Completed },
[SprintStatus.Canceled]: { icon: tracker.icon.SprintStatusCanceled, label: tracker.string.Canceled }
}
export const defaultPriorities = [
IssuePriority.NoPriority,
IssuePriority.Urgent,

View File

@ -24,12 +24,14 @@ import {
IssuesOrdering,
IssueStatus,
ProjectStatus,
Sprint,
SprintStatus,
Team
} from '@anticrm/tracker'
import { ViewOptionModel } from '@anticrm/view-resources'
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
import tracker from './plugin'
import { defaultPriorities, defaultProjectStatuses, issuePriorities } from './types'
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
export * from './types'
@ -223,6 +225,8 @@ export const getDueDateIconModifier = (
export type ProjectsViewMode = 'all' | 'backlog' | 'active' | 'closed'
export type SprintViewMode = 'all' | 'planned' | 'active' | 'closed'
export const getIncludedProjectStatuses = (mode: ProjectsViewMode): ProjectStatus[] => {
switch (mode) {
case 'all': {
@ -243,6 +247,26 @@ export const getIncludedProjectStatuses = (mode: ProjectsViewMode): ProjectStatu
}
}
export const getIncludedSprintStatuses = (mode: SprintViewMode): SprintStatus[] => {
switch (mode) {
case 'all': {
return defaultSprintStatuses
}
case 'active': {
return [SprintStatus.InProgress]
}
case 'planned': {
return [SprintStatus.Planned]
}
case 'closed': {
return [SprintStatus.Completed, SprintStatus.Canceled]
}
default: {
return []
}
}
}
export const projectsTitleMap: Record<ProjectsViewMode, IntlString> = Object.freeze({
all: tracker.string.AllProjects,
backlog: tracker.string.BacklogProjects,
@ -250,6 +274,13 @@ export const projectsTitleMap: Record<ProjectsViewMode, IntlString> = Object.fre
closed: tracker.string.ClosedProjects
})
export const sprintTitleMap: Record<SprintViewMode, IntlString> = Object.freeze({
all: tracker.string.AllSprints,
planned: tracker.string.PlannedSprints,
active: tracker.string.ActiveSprints,
closed: tracker.string.ClosedSprints
})
const listIssueStatusOrder = [
tracker.issueStatusCategory.Started,
tracker.issueStatusCategory.Unstarted,
@ -482,3 +513,16 @@ export function getDefaultViewOptionsConfig (): ViewOptionModel[] {
}
]
}
/**
* @public
*/
export function getSprintDays (value: Sprint): string {
const st = new Date(value.startDate).getDate()
const days = Math.floor(Math.abs((1 + value.targetDate - value.startDate) / 1000 / 60 / 60 / 24)) + 1
const stDate = new Date(value.startDate)
let ds = Array.from(Array(days).keys()).map((it) => st + it)
ds = ds.filter((it) => ![0, 6].includes(new Date(stDate.setDate(it)).getDay()))
return ds.join(' ')
}

View File

@ -96,6 +96,36 @@ export enum IssuesDateModificationPeriod {
PastMonth = 'pastMonth'
}
/**
* @public
*/
export enum SprintStatus {
Planned,
InProgress,
Completed,
Canceled
}
/**
* @public
*/
export interface Sprint extends Doc {
label: string
description?: Markup
status: SprintStatus
lead: Ref<Employee> | null
space: Ref<Team>
comments: number
attachments?: number
startDate: Timestamp
targetDate: Timestamp
}
/**
* @public
*/
@ -124,6 +154,8 @@ export interface Issue extends AttachedDoc {
dueDate: Timestamp | null
rank: string
sprint?: Ref<Sprint> | null
}
/**
@ -210,7 +242,9 @@ export default plugin(trackerId, {
IssueStatus: '' as Ref<Class<IssueStatus>>,
IssueStatusCategory: '' as Ref<Class<IssueStatusCategory>>,
TypeIssuePriority: '' as Ref<Class<Type<IssuePriority>>>,
TypeProjectStatus: '' as Ref<Class<Type<ProjectStatus>>>
TypeProjectStatus: '' as Ref<Class<Type<ProjectStatus>>>,
Sprint: '' as Ref<Class<Sprint>>,
TypeSprintStatus: '' as Ref<Class<Type<SprintStatus>>>
},
ids: {
NoParent: '' as Ref<Issue>
@ -243,6 +277,7 @@ export default plugin(trackerId, {
Labels: '' as Asset,
DueDate: '' as Asset,
Parent: '' as Asset,
Sprint: '' as Asset,
CategoryBacklog: '' as Asset,
CategoryUnstarted: '' as Asset,
@ -267,6 +302,12 @@ export default plugin(trackerId, {
ProjectStatusCompleted: '' as Asset,
ProjectStatusCanceled: '' as Asset,
SprintStatusPlanned: '' as Asset,
SprintStatusInProgress: '' as Asset,
SprintStatusPaused: '' as Asset,
SprintStatusCompleted: '' as Asset,
SprintStatusCanceled: '' as Asset,
CopyID: '' as Asset,
CopyURL: '' as Asset,
CopyBranch: '' as Asset
@ -288,7 +329,8 @@ export default plugin(trackerId, {
MoveToTeam: '' as Ref<Action>,
Relations: '' as Ref<Action>,
NewSubIssue: '' as Ref<Action>,
EditWorkflowStatuses: '' as Ref<Action>
EditWorkflowStatuses: '' as Ref<Action>,
SetSprint: '' as Ref<Action>
},
team: {
DefaultTeam: '' as Ref<Team>

View File

@ -33,8 +33,12 @@
const dispatch = createEventDispatcher()
const changeStatus = async (newStatus: any) => {
if (!isEditable || newStatus == null) {
dispatch('close', null)
if (newStatus === '#null') {
newStatus = null
return
}
if (!isEditable || newStatus === undefined) {
dispatch('close', undefined)
return
}
const docs = Array.isArray(value) ? value : [value]
@ -106,12 +110,14 @@
$: updateQuery(query, value, fillQuery)
$: huge = size === 'medium' || size === 'large'
$: valuesToShow = values !== undefined ? values.map((it) => ({ ...it, isSelected: it.id === current })) : []
</script>
{#if docMatch}
{#if values}
<SelectPopup
value={values.map((it) => ({ ...it, isSelected: it.id === current }))}
value={valuesToShow}
on:close={(evt) => changeStatus(evt.detail)}
placeholder={placeholder ?? view.string.Filter}
searchable
@ -126,7 +132,7 @@
{searchField}
allowDeselect={true}
selected={current}
on:close={(evt) => changeStatus(evt.detail?._id)}
on:close={(evt) => changeStatus(evt.detail === null ? null : evt.detail?._id)}
placeholder={placeholder ?? view.string.Filter}
{width}
{size}

View File

@ -491,11 +491,11 @@
"projectFolder": "dev/prod",
"shouldPublish": false
},
{
"packageName": "@anticrm/prod-tracker",
"projectFolder": "products/tracker",
"shouldPublish": false
},
// {
// "packageName": "@anticrm/prod-tracker",
// "projectFolder": "products/tracker",
// "shouldPublish": false
// },
{
"packageName": "@anticrm/server-core",
"projectFolder": "server/core",

View File

@ -29,11 +29,11 @@ test.describe('project tests', () => {
await page.click('button:has-text("New issue")')
await page.fill('[placeholder="Issue\\ title"]', 'issue')
await page.click('form button:has-text("Project")')
await page.click(`button:has-text("${prjId}")`)
await page.click('button:has-text("Save issue")')
await page.click(`.popup button:has-text("${prjId}")`)
await page.click('form button:has-text("Save issue")')
await page.waitForSelector('form.antiCard', { state: 'detached' })
await page.click(`button:has-text("${prjId}")`)
await page.click('button:has-text("No project")')
await page.click(`.gridElement button:has-text("${prjId}")`)
await page.click('.popup button:has-text("No project")')
})
test('create-project-with-status', async ({ page }) => {