mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-31 20:57:31 +00:00
Initial Sprints support (#2246)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
29775429d5
commit
00131ed0a3
@ -2,6 +2,10 @@
|
||||
|
||||
## 0.6.32 (upcoming)
|
||||
|
||||
Tracker:
|
||||
|
||||
- Basic sprints
|
||||
|
||||
## 0.6.31
|
||||
|
||||
Core:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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); }
|
||||
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
import { resizeObserver } from '../resize'
|
||||
|
||||
interface ValueType {
|
||||
id: number | string
|
||||
id: number | string | null
|
||||
icon?: Asset
|
||||
iconColor?: string
|
||||
label?: IntlString
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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 |
@ -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": {}
|
||||
}
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -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`
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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={() => {}} />
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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) ?? ''}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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}
|
||||
/>
|
@ -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}
|
@ -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>
|
@ -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)
|
||||
}}
|
||||
/>
|
@ -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>
|
@ -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>
|
@ -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}
|
||||
/>
|
@ -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}
|
@ -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}
|
||||
/>
|
@ -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}
|
@ -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}
|
@ -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)
|
||||
|
@ -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>>
|
||||
|
@ -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,
|
||||
|
@ -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(' ')
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
10
rush.json
10
rush.json
@ -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",
|
||||
|
@ -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 }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user