TSK-413: Implement scrum recording (#2550)

Signed-off-by: Anton Brechka <anton.brechka@xored.com>
This commit is contained in:
mrsadman99 2023-02-03 12:47:25 +07:00 committed by GitHub
parent 7f05cbe497
commit b0b1089369
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 2139 additions and 335 deletions

View File

@ -16,8 +16,7 @@
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import { calendarId, Calendar, Event, Reminder } from '@hcengineering/calendar' import { calendarId, Calendar, Event, Reminder } from '@hcengineering/calendar'
import { Employee } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import type { Domain, Markup, Ref, Timestamp } from '@hcengineering/core' import { DateRangeMode, Domain, Markup, Ref, Timestamp, IndexKind } from '@hcengineering/core'
import { IndexKind } from '@hcengineering/core'
import { import {
ArrOf, ArrOf,
Builder, Builder,
@ -66,10 +65,10 @@ export class TEvent extends TAttachedDoc implements Event {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
location?: string location?: string
@Prop(TypeDate(true), calendar.string.Date) @Prop(TypeDate(DateRangeMode.DATETIME), calendar.string.Date)
date!: Timestamp date!: Timestamp
@Prop(TypeDate(true), calendar.string.DueTo) @Prop(TypeDate(DateRangeMode.DATETIME), calendar.string.DueTo)
dueDate!: Timestamp dueDate!: Timestamp
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
@ -85,7 +84,7 @@ export class TEvent extends TAttachedDoc implements Event {
@Mixin(calendar.mixin.Reminder, calendar.class.Event) @Mixin(calendar.mixin.Reminder, calendar.class.Event)
@UX(calendar.string.Reminder, calendar.icon.Calendar) @UX(calendar.string.Reminder, calendar.icon.Calendar)
export class TReminder extends TEvent implements Reminder { export class TReminder extends TEvent implements Reminder {
@Prop(TypeDate(true), calendar.string.Shift) @Prop(TypeDate(DateRangeMode.DATETIME), calendar.string.Shift)
@Hidden() @Hidden()
shift!: Timestamp shift!: Timestamp

View File

@ -30,8 +30,7 @@ import {
Persons, Persons,
Status Status
} from '@hcengineering/contact' } from '@hcengineering/contact'
import type { Class, Domain, Ref, Timestamp } from '@hcengineering/core' import { Class, DateRangeMode, Domain, Ref, Timestamp, DOMAIN_MODEL, IndexKind } from '@hcengineering/core'
import { DOMAIN_MODEL, IndexKind } from '@hcengineering/core'
import { import {
Builder, Builder,
Collection, Collection,
@ -115,7 +114,7 @@ export class TChannel extends TAttachedDoc implements Channel {
@Model(contact.class.Person, contact.class.Contact) @Model(contact.class.Person, contact.class.Contact)
@UX(contact.string.Person, contact.icon.Person, undefined, 'name') @UX(contact.string.Person, contact.icon.Person, undefined, 'name')
export class TPerson extends TContact implements Person { export class TPerson extends TContact implements Person {
@Prop(TypeDate(false, false), contact.string.Birthday) @Prop(TypeDate(DateRangeMode.DATE, false), contact.string.Birthday)
birthday?: Timestamp birthday?: Timestamp
} }

View File

@ -13,9 +13,10 @@
// limitations under the License. // limitations under the License.
// //
import type { Employee } from '@hcengineering/contact' import type { Employee, EmployeeAccount } from '@hcengineering/contact'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { import {
DateRangeMode,
Domain, Domain,
DOMAIN_MODEL, DOMAIN_MODEL,
FindOptions, FindOptions,
@ -41,6 +42,7 @@ import {
TypeNumber, TypeNumber,
TypeRef, TypeRef,
TypeString, TypeString,
TypeTimestamp,
UX UX
} from '@hcengineering/model' } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment' import attachment from '@hcengineering/model-attachment'
@ -65,6 +67,8 @@ import {
IssueTemplateChild, IssueTemplateChild,
Project, Project,
ProjectStatus, ProjectStatus,
Scrum,
ScrumRecord,
Sprint, Sprint,
SprintStatus, SprintStatus,
Team, Team,
@ -242,7 +246,7 @@ export class TIssue extends TAttachedDoc implements Issue {
declare space: Ref<Team> declare space: Ref<Team>
@Prop(TypeDate(true), tracker.string.DueDate) @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate)
dueDate!: Timestamp | null dueDate!: Timestamp | null
@Prop(TypeString(), tracker.string.Rank) @Prop(TypeString(), tracker.string.Rank)
@ -296,7 +300,7 @@ export class TIssueTemplate extends TDoc implements IssueTemplate {
declare space: Ref<Team> declare space: Ref<Team>
@Prop(TypeDate(true), tracker.string.DueDate) @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate)
dueDate!: Timestamp | null dueDate!: Timestamp | null
@Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint) @Prop(TypeRef(tracker.class.Sprint), tracker.string.Sprint)
@ -396,10 +400,10 @@ export class TProject extends TDoc implements Project {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number attachments?: number
@Prop(TypeDate(true), tracker.string.StartDate) @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.StartDate)
startDate!: Timestamp | null startDate!: Timestamp | null
@Prop(TypeDate(true), tracker.string.TargetDate) @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.TargetDate)
targetDate!: Timestamp | null targetDate!: Timestamp | null
declare space: Ref<Team> declare space: Ref<Team>
@ -433,10 +437,10 @@ export class TSprint extends TDoc implements Sprint {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number attachments?: number
@Prop(TypeDate(false), tracker.string.StartDate) @Prop(TypeDate(), tracker.string.StartDate)
startDate!: Timestamp startDate!: Timestamp
@Prop(TypeDate(false), tracker.string.TargetDate) @Prop(TypeDate(), tracker.string.TargetDate)
targetDate!: Timestamp targetDate!: Timestamp
declare space: Ref<Team> declare space: Ref<Team>
@ -448,6 +452,62 @@ export class TSprint extends TDoc implements Sprint {
project!: Ref<Project> project!: Ref<Project>
} }
/**
* @public
*/
@Model(tracker.class.Scrum, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Scrum, tracker.icon.Scrum, tracker.string.Scrum)
export class TScrum extends TDoc implements Scrum {
@Prop(TypeString(), tracker.string.Title)
title!: string
@Prop(TypeMarkup(), tracker.string.Description)
description?: Markup
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(ArrOf(TypeRef(contact.class.Employee)), tracker.string.Members)
members!: Ref<Employee>[]
@Prop(Collection(tracker.class.Scrum), tracker.string.ScrumRecords)
scrumRecords?: number
@Prop(TypeDate(DateRangeMode.TIME), tracker.string.ScrumBeginTime)
beginTime!: Timestamp
@Prop(TypeDate(DateRangeMode.TIME), tracker.string.ScrumEndTime)
endTime!: Timestamp
declare space: Ref<Team>
}
/**
* @public
*/
@Model(tracker.class.ScrumRecord, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.ScrumRecord, tracker.icon.Scrum, tracker.string.ScrumRecord)
export class TScrumRecord extends TAttachedDoc implements ScrumRecord {
@Prop(TypeString(), tracker.string.Title)
label!: string
@Prop(TypeTimestamp(), tracker.string.ScrumBeginTime)
startTs!: Timestamp
@Prop(TypeTimestamp(), tracker.string.ScrumEndTime)
endTs?: Timestamp
@Prop(Collection(chunter.class.Comment), tracker.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), tracker.string.Attachments)
attachments!: number
declare attachedTo: Ref<Scrum>
declare space: Ref<Team>
declare scrumRecorder: Ref<EmployeeAccount>
}
@UX(core.string.Number) @UX(core.string.Number)
@Model(tracker.class.TypeReportedTime, core.class.Type) @Model(tracker.class.TypeReportedTime, core.class.Type)
export class TTypeReportedTime extends TType {} export class TTypeReportedTime extends TType {}
@ -463,6 +523,8 @@ export function createModel (builder: Builder): void {
TTypeIssuePriority, TTypeIssuePriority,
TTypeProjectStatus, TTypeProjectStatus,
TSprint, TSprint,
TScrum,
TScrumRecord,
TTypeSprintStatus, TTypeSprintStatus,
TTimeSpendReport, TTimeSpendReport,
TTypeReportedTime TTypeReportedTime
@ -747,6 +809,7 @@ export function createModel (builder: Builder): void {
const projectsId = 'projects' const projectsId = 'projects'
const sprintsId = 'sprints' const sprintsId = 'sprints'
const templatesId = 'templates' const templatesId = 'templates'
const scrumsId = 'scrums'
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.IssuePresenter presenter: tracker.component.IssuePresenter
@ -805,7 +868,7 @@ export function createModel (builder: Builder): void {
}) })
builder.mixin(tracker.class.Project, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(tracker.class.Project, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.ProjectTitlePresenter presenter: tracker.component.ProjectPresenter
}) })
builder.mixin(tracker.class.Team, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(tracker.class.Team, core.class.Class, view.mixin.ObjectPresenter, {
@ -819,7 +882,7 @@ export function createModel (builder: Builder): void {
}) })
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ObjectPresenter, {
presenter: tracker.component.SprintTitlePresenter presenter: tracker.component.SprintPresenter
}) })
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributePresenter, {
@ -917,6 +980,12 @@ export function createModel (builder: Builder): void {
icon: tracker.icon.Sprint, icon: tracker.icon.Sprint,
component: tracker.component.Sprints component: tracker.component.Sprints
}, },
{
id: scrumsId,
label: tracker.string.Scrums,
icon: tracker.icon.Scrum,
component: tracker.component.Scrums
},
{ {
id: templatesId, id: templatesId,
label: tracker.string.IssueTemplates, label: tracker.string.IssueTemplates,

View File

@ -41,7 +41,6 @@ export default mergeIds(trackerId, tracker, {
// Required to pass build without errorsF // Required to pass build without errorsF
Nope: '' as AnyComponent, Nope: '' as AnyComponent,
SprintSelector: '' as AnyComponent, SprintSelector: '' as AnyComponent,
SubIssuesSelector: '' as AnyComponent,
IssueStatistics: '' as AnyComponent IssueStatistics: '' as AnyComponent
}, },
app: { app: {

View File

@ -196,12 +196,20 @@ export type AttachedData<T extends AttachedDoc> = Omit<T, keyof AttachedDoc>
/** /**
* @public * @public
*/ */
export enum DateRangeMode {
DATE = 'date',
TIME = 'time',
DATETIME = 'datetime'
}
/**
* @public
*/
export interface TypeDate extends Type<Date> { export interface TypeDate extends Type<Date> {
// If not set date mode default
mode: DateRangeMode
// If not set to true, will be false // If not set to true, will be false
withTime?: boolean withShift: boolean
// If not set to true, will be false
withShift?: boolean
} }
/** /**

View File

@ -23,6 +23,7 @@ import core, {
ClassifierKind, ClassifierKind,
Collection as TypeCollection, Collection as TypeCollection,
Data, Data,
DateRangeMode,
Doc, Doc,
Domain, Domain,
Enum, Enum,
@ -405,8 +406,8 @@ export function TypeTimestamp (): Type<Timestamp> {
/** /**
* @public * @public
*/ */
export function TypeDate (withTime?: boolean, withShift?: boolean): TypeDateType { export function TypeDate (mode: DateRangeMode = DateRangeMode.DATE, withShift: boolean = true): TypeDateType {
return { _class: core.class.TypeDate, label: core.string.Date, withTime, withShift } return { _class: core.class.TypeDate, label: core.string.Date, mode, withShift }
} }
/** /**

View File

@ -34,6 +34,7 @@
export let isHeader: boolean = true export let isHeader: boolean = true
export let isSub: boolean = true export let isSub: boolean = true
export let isAside: boolean = true export let isAside: boolean = true
export let isUtils: boolean = true
export let isCustomAttr: boolean = true export let isCustomAttr: boolean = true
export let floatAside = false export let floatAside = false
export let allowClose = true export let allowClose = true
@ -89,7 +90,7 @@
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
<Component is={calendar.component.DocReminder} props={{ value: object, title }} /> <Component is={calendar.component.DocReminder} props={{ value: object, title }} />
<Component is={notification.component.LastViewEditor} props={{ value: object }} /> <Component is={notification.component.LastViewEditor} props={{ value: object }} />
{#if $$slots.utils} {#if isUtils && $$slots.utils}
<div class="buttons-divider" /> <div class="buttons-divider" />
<slot name="utils" /> <slot name="utils" />
{/if} {/if}

View File

@ -14,6 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Asset, IntlString } from '@hcengineering/platform' import type { Asset, IntlString } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import { getFocusManager } from '../focus' import { getFocusManager } from '../focus'
import { showPopup } from '../popups' import { showPopup } from '../popups'
import type { AnySvelteComponent, ButtonKind, ButtonSize, ListItem, TooltipAlignment } from '../types' import type { AnySvelteComponent, ButtonKind, ButtonSize, ListItem, TooltipAlignment } from '../types'
@ -37,6 +38,7 @@
let container: HTMLElement let container: HTMLElement
let opened: boolean = false let opened: boolean = false
const dispatch = createEventDispatcher()
const mgr = getFocusManager() const mgr = getFocusManager()
</script> </script>
@ -53,7 +55,10 @@
if (!opened) { if (!opened) {
opened = true opened = true
showPopup(DropdownPopup, { title: label, items, icon }, container, (result) => { showPopup(DropdownPopup, { title: label, items, icon }, container, (result) => {
if (result) selected = result if (result) {
selected = result
dispatch('selected', result)
}
opened = false opened = false
mgr?.setFocusPos(focusIndex) mgr?.setFocusPos(focusIndex)
}) })

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { DateRangeMode } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { showPopup } from '../popups' import { showPopup } from '../popups'
@ -75,7 +76,7 @@
{#if value?.shift !== undefined} {#if value?.shift !== undefined}
<TimeShiftPresenter value={value.shift} /> <TimeShiftPresenter value={value.shift} />
{:else} {:else}
<DateRangePresenter value={value?.date} withTime={true} editable={false} /> <DateRangePresenter value={value?.date} mode={DateRangeMode.DATETIME} editable={false} />
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { DateRangeMode } from '@hcengineering/core'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import ui from '../plugin' import ui from '../plugin'
import { DateOrShift } from '../types' import { DateOrShift } from '../types'
@ -39,7 +40,7 @@
<div class="flex-center mt-1 mb-1"> <div class="flex-center mt-1 mb-1">
<DateRangePresenter <DateRangePresenter
bind:value={date} bind:value={date}
withTime={true} mode={DateRangeMode.DATETIME}
editable={true} editable={true}
labelNull={ui.string.SelectDate} labelNull={ui.string.SelectDate}
on:change={() => { on:change={() => {

View File

@ -14,6 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { DateRangeMode } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import ui from '../../plugin' import ui from '../../plugin'
import Label from '../Label.svelte' import Label from '../Label.svelte'
@ -34,11 +35,13 @@
dispatch('change', value) dispatch('change', value)
} }
} }
$: mode = withTime ? DateRangeMode.DATETIME : DateRangeMode.DATE
</script> </script>
<div class="antiSelect antiWrapper cursor-default"> <div class="antiSelect antiWrapper cursor-default">
<div class="flex-col"> <div class="flex-col">
<span class="label mb-1"><Label label={title} /></span> <span class="label mb-1"><Label label={title} /></span>
<DatePresenter {value} {withTime} {icon} {labelOver} {labelNull} editable on:change={changeValue} /> <DatePresenter {value} {mode} {icon} {labelOver} {labelNull} editable on:change={changeValue} />
</div> </div>
</div> </div>

View File

@ -32,6 +32,7 @@
} }
$: component = $dpstore.component $: component = $dpstore.component
$: shift = $dpstore.shift $: shift = $dpstore.shift
$: mode = $dpstore.mode
function _update (result: any): void { function _update (result: any): void {
fitPopup() fitPopup()
@ -106,6 +107,7 @@
{#if component} {#if component}
<svelte:component <svelte:component
this={component} this={component}
bind:mode
bind:shift bind:shift
bind:this={componentInstance} bind:this={componentInstance}
on:update={(ev) => _update(ev.detail)} on:update={(ev) => _update(ev.detail)}

View File

@ -24,9 +24,10 @@
import DPCalendarOver from './icons/DPCalendarOver.svelte' import DPCalendarOver from './icons/DPCalendarOver.svelte'
import { getMonthName } from './internal/DateUtils' import { getMonthName } from './internal/DateUtils'
import DatePopup from './DatePopup.svelte' import DatePopup from './DatePopup.svelte'
import { DateRangeMode } from '@hcengineering/core'
export let value: number | null | undefined export let value: number | null | undefined
export let withTime: boolean = false export let mode: DateRangeMode = DateRangeMode.DATE
export let mondayStart: boolean = true export let mondayStart: boolean = true
export let editable: boolean = false export let editable: boolean = false
export let icon: 'normal' | 'warning' | 'critical' | 'overdue' = 'normal' export let icon: 'normal' | 'warning' | 'critical' | 'overdue' = 'normal'
@ -58,6 +59,8 @@
dispatch('change', value) dispatch('change', value)
opened = false opened = false
} }
$: withTime = mode !== DateRangeMode.DATE
</script> </script>
<button <button

View File

@ -18,6 +18,7 @@
import ui from '../../plugin' import ui from '../../plugin'
import Label from '../Label.svelte' import Label from '../Label.svelte'
import DateRangePresenter from './DateRangePresenter.svelte' import DateRangePresenter from './DateRangePresenter.svelte'
import { DateRangeMode } from '@hcengineering/core'
export let title: IntlString export let title: IntlString
export let value: number | null | undefined = null export let value: number | null | undefined = null
@ -39,6 +40,14 @@
<div class="antiSelect antiWrapper cursor-default"> <div class="antiSelect antiWrapper cursor-default">
<div class="flex-col"> <div class="flex-col">
<span class="label mb-1"><Label label={title} /></span> <span class="label mb-1"><Label label={title} /></span>
<DateRangePresenter {value} {withTime} {icon} {labelOver} {labelNull} editable on:change={changeValue} /> <DateRangePresenter
{value}
mode={DateRangeMode.DATETIME}
{icon}
{labelOver}
{labelNull}
editable
on:change={changeValue}
/>
</div> </div>
</div> </div>

View File

@ -18,46 +18,69 @@
import Month from './Month.svelte' import Month from './Month.svelte'
import Scroller from '../Scroller.svelte' import Scroller from '../Scroller.svelte'
import TimeShiftPresenter from '../TimeShiftPresenter.svelte' import TimeShiftPresenter from '../TimeShiftPresenter.svelte'
import { DateRangeMode } from '@hcengineering/core'
export let direction: 'before' | 'after' = 'after' export let direction: 'before' | 'after' = 'after'
export let minutes: number[] = [5, 15, 30] export let minutes: number[] = [5, 15, 30]
export let hours: number[] = [1, 2, 4, 8, 12] export let hours: number[] = [1, 2, 4, 8, 12]
export let days: number[] = [1, 3, 7, 30] export let days: number[] = [1, 3, 7, 30]
export let shift: boolean = false export let shift: boolean = false
export let mode: DateRangeMode = DateRangeMode.DATE
$: withTime = mode !== DateRangeMode.DATE
$: withDate = mode !== DateRangeMode.TIME
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const today: Date = new Date(Date.now()) const today = new Date(Date.now())
$: currentDate = $dpstore.currentDate ?? today const startDate = new Date(0)
$: defaultDate =
mode === DateRangeMode.TIME
? new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate(),
today.getHours(),
today.getMinutes()
)
: today
$: currentDate = $dpstore.currentDate ?? defaultDate
const mondayStart: boolean = true const mondayStart: boolean = true
$: base = direction === 'before' ? -1 : 1 $: base = direction === 'before' ? -1 : 1
const MINUTE = 60 * 1000 const MINUTE = 60 * 1000
const HOUR = 60 * MINUTE const HOUR = 60 * MINUTE
const DAY = 24 * HOUR const DAY = 24 * HOUR
$: values = [
...minutes.map((m) => m * MINUTE), const shiftValues: (number | string)[] = []
'divider',
...hours.map((m) => m * HOUR), $: {
'divider', if (withTime) {
...days.map((m) => m * DAY) shiftValues.push(...minutes.map((m) => m * MINUTE), 'divider', ...hours.map((m) => m * HOUR))
] }
if (withDate) {
shiftValues.push('divider', ...days.map((m) => m * DAY))
}
}
</script> </script>
<div class="month-popup-container"> <div class="month-popup-container">
<Month {#if mode !== DateRangeMode.TIME}
bind:currentDate <Month
{mondayStart} bind:currentDate
on:update={(result) => { {mondayStart}
if (result.detail !== undefined) { on:update={(result) => {
dispatch('close', result.detail) if (result.detail !== undefined) {
} dispatch('close', result.detail)
}} }
/> }}
/>
{/if}
{#if shift} {#if shift}
<div class="shift-container"> <div class="shift-container">
<Scroller> <Scroller>
{#each values as value} {#each shiftValues as value}
{#if typeof value === 'number'} {#if typeof value === 'number'}
<div <div
class="btn" class="btn"
@ -92,6 +115,7 @@
top: 1rem; top: 1rem;
right: calc(100% - 0.5rem); right: calc(100% - 0.5rem);
bottom: 1rem; bottom: 1rem;
height: fit-content;
width: fit-content; width: fit-content;
width: 12rem; width: 12rem;
min-width: 12rem; min-width: 12rem;

View File

@ -24,9 +24,10 @@
import DPCalendar from './icons/DPCalendar.svelte' import DPCalendar from './icons/DPCalendar.svelte'
import DPCalendarOver from './icons/DPCalendarOver.svelte' import DPCalendarOver from './icons/DPCalendarOver.svelte'
import DateRangePopup from './DateRangePopup.svelte' import DateRangePopup from './DateRangePopup.svelte'
import { DateRangeMode } from '@hcengineering/core'
export let value: number | null | undefined = null export let value: number | null | undefined = null
export let withTime: boolean = false export let mode: DateRangeMode = DateRangeMode.DATE
export let editable: boolean = false export let editable: boolean = false
export let icon: 'normal' | 'warning' | 'overdue' = 'normal' export let icon: 'normal' | 'warning' | 'overdue' = 'normal'
export let labelOver: IntlString | undefined = undefined // label instead of date export let labelOver: IntlString | undefined = undefined // label instead of date
@ -44,9 +45,12 @@
} }
const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min'] const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min']
const getIndex = (id: TEdits): number => editsType.indexOf(id) const getIndex = (id: TEdits): number => editsType.indexOf(id)
const today: Date = new Date(Date.now()) const today = new Date(Date.now())
const startDate = new Date(0)
const defaultSelected: TEdits = mode === DateRangeMode.TIME ? 'hour' : 'day'
let currentDate: Date let currentDate: Date
let selected: TEdits = 'day' let selected: TEdits = defaultSelected
let edit: boolean = false let edit: boolean = false
let opened: boolean = false let opened: boolean = false
@ -58,6 +62,9 @@
return { id: edit, value: -1 } return { id: edit, value: -1 }
}) })
$: withTime = mode !== DateRangeMode.DATE
$: withDate = mode !== DateRangeMode.TIME
const getValue = (date: Date | null | undefined = today, id: TEdits): number => { const getValue = (date: Date | null | undefined = today, id: TEdits): number => {
switch (id) { switch (id) {
case 'day': case 'day':
@ -126,7 +133,7 @@
edits.forEach((edit, i) => { edits.forEach((edit, i) => {
tempValues[i] = edit.value > 0 || i > 2 ? edit.value : getValue(currentDate, edit.id) tempValues[i] = edit.value > 0 || i > 2 ? edit.value : getValue(currentDate, edit.id)
}) })
currentDate = new Date(tempValues[2], tempValues[1] - 1, tempValues[0], tempValues[3], tempValues[4]) setCurrentDate(new Date(tempValues[2], tempValues[1] - 1, tempValues[0], tempValues[3], tempValues[4]))
} }
const isNull = (full: boolean = false): boolean => { const isNull = (full: boolean = false): boolean => {
let result: boolean = false let result: boolean = false
@ -162,7 +169,7 @@
if (!isNull() && edits[2].value > 999) { if (!isNull() && edits[2].value > 999) {
fixEdits() fixEdits()
currentDate = setValue(edits[index].value, currentDate, ed) setCurrentDate(setValue(edits[index].value, currentDate, ed))
$dpstore.currentDate = currentDate $dpstore.currentDate = currentDate
dateToEdits() dateToEdits()
} }
@ -183,17 +190,29 @@
if (edits[index].value !== -1) { if (edits[index].value !== -1) {
const val = ev.code === 'ArrowUp' ? edits[index].value + 1 : edits[index].value - 1 const val = ev.code === 'ArrowUp' ? edits[index].value + 1 : edits[index].value - 1
if (currentDate) { if (currentDate) {
currentDate = setValue(val, currentDate, ed) setCurrentDate(setValue(val, currentDate, ed))
$dpstore.currentDate = currentDate $dpstore.currentDate = currentDate
dateToEdits() dateToEdits()
} }
} }
} }
if (ev.code === 'ArrowLeft' && edits[index].el) { if (ev.code === 'ArrowLeft' && edits[index].el) {
selected = index === 0 ? edits[withTime ? 4 : 2].id : edits[index - 1].id if (mode === DateRangeMode.TIME) {
selected = index === 3 ? edits[4].id : edits[index - 1].id
} else if (mode === DateRangeMode.DATETIME) {
selected = index === 0 ? edits[4].id : edits[index - 1].id
} else {
selected = index === 0 ? edits[2].id : edits[index - 1].id
}
} }
if (ev.code === 'ArrowRight' && edits[index].el) { if (ev.code === 'ArrowRight' && edits[index].el) {
selected = index === (withTime ? 4 : 2) ? edits[0].id : edits[index + 1].id if (mode === DateRangeMode.TIME) {
selected = index === 4 ? edits[3].id : edits[index + 1].id
} else if (mode === DateRangeMode.DATETIME) {
selected = index === 4 ? edits[0].id : edits[index + 1].id
} else {
selected = index === 2 ? edits[0].id : edits[index + 1].id
}
} }
if (ev.code === 'Tab') { if (ev.code === 'Tab') {
if ((ed === 'year' && !withTime) || (ed === 'min' && withTime)) closeDP() if ((ed === 'year' && !withTime) || (ed === 'min' && withTime)) closeDP()
@ -220,16 +239,39 @@
if (tempEl) tempEl.focus() if (tempEl) tempEl.focus()
}) })
const setEmptyEdits = () => {
edits.forEach((edit, index) => {
if (mode !== DateRangeMode.TIME || index > 2) {
edit.value = -1
} else {
edit.value = getValue(startDate, edit.id)
}
})
edits = edits
}
const setCurrentDate = (date: Date) => {
if (mode === DateRangeMode.TIME) {
const resultDate = new Date(startDate)
resultDate.setHours(date.getHours())
resultDate.setMinutes(date.getMinutes())
currentDate = resultDate
} else {
currentDate = date
}
}
const _change = (result: any): void => { const _change = (result: any): void => {
if (result !== undefined) { if (result !== undefined) {
currentDate = result setCurrentDate(result)
saveDate() saveDate()
} }
} }
const _close = (result: any): void => { const _close = (result: any): void => {
if (result !== undefined) { if (result !== undefined) {
if (result !== null) { if (result !== null) {
currentDate = result setCurrentDate(result)
saveDate() saveDate()
} }
closeDP() closeDP()
@ -244,6 +286,7 @@
$dpstore.onClose = _close $dpstore.onClose = _close
$dpstore.component = DateRangePopup $dpstore.component = DateRangePopup
$dpstore.shift = !noShift $dpstore.shift = !noShift
$dpstore.mode = mode
} }
let popupComp: HTMLElement let popupComp: HTMLElement
$: if (opened && $dpstore.popup) popupComp = $dpstore.popup $: if (opened && $dpstore.popup) popupComp = $dpstore.popup
@ -257,15 +300,12 @@
} }
export const adaptValue = () => { export const adaptValue = () => {
currentDate = new Date(value ?? Date.now()) setCurrentDate(new Date(value ?? Date.now()))
currentDate.setSeconds(0, 0) currentDate.setSeconds(0, 0)
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
dateToEdits() dateToEdits()
} else if (value === null) { } else if (value === null) {
edits.forEach((edit) => { setEmptyEdits()
edit.value = -1
})
currentDate = today
} }
} }
@ -283,46 +323,50 @@
}} }}
> >
{#if edit} {#if edit}
<span {#if withDate}
bind:this={edits[0].el} <span
class="digit" bind:this={edits[0].el}
tabindex="0" class="digit"
on:keydown={(ev) => keyDown(ev, edits[0].id)} tabindex="0"
on:focus={() => focused(edits[0].id)} on:keydown={(ev) => keyDown(ev, edits[0].id)}
on:blur={(ev) => unfocus(ev, edits[0].id)} on:focus={() => focused(edits[0].id)}
> on:blur={(ev) => unfocus(ev, edits[0].id)}
{#if edits[0].value > -1} >
{edits[0].value.toString().padStart(2, '0')} {#if edits[0].value > -1}
{:else}DD{/if} {edits[0].value.toString().padStart(2, '0')}
</span> {:else}DD{/if}
<span class="separator">.</span> </span>
<span <span class="separator">.</span>
bind:this={edits[1].el} <span
class="digit" bind:this={edits[1].el}
tabindex="0" class="digit"
on:keydown={(ev) => keyDown(ev, edits[1].id)} tabindex="0"
on:focus={() => focused(edits[1].id)} on:keydown={(ev) => keyDown(ev, edits[1].id)}
on:blur={(ev) => unfocus(ev, edits[1].id)} on:focus={() => focused(edits[1].id)}
> on:blur={(ev) => unfocus(ev, edits[1].id)}
{#if edits[1].value > -1} >
{edits[1].value.toString().padStart(2, '0')} {#if edits[1].value > -1}
{:else}MM{/if} {edits[1].value.toString().padStart(2, '0')}
</span> {:else}MM{/if}
<span class="separator">.</span> </span>
<span <span class="separator">.</span>
bind:this={edits[2].el} <span
class="digit" bind:this={edits[2].el}
tabindex="0" class="digit"
on:keydown={(ev) => keyDown(ev, edits[2].id)} tabindex="0"
on:focus={() => focused(edits[2].id)} on:keydown={(ev) => keyDown(ev, edits[2].id)}
on:blur={(ev) => unfocus(ev, edits[2].id)} on:focus={() => focused(edits[2].id)}
> on:blur={(ev) => unfocus(ev, edits[2].id)}
{#if edits[2].value > -1} >
{edits[2].value.toString().padStart(4, '0')} {#if edits[2].value > -1}
{:else}YYYY{/if} {edits[2].value.toString().padStart(4, '0')}
</span> {:else}YYYY{/if}
</span>
{/if}
{#if withTime} {#if withTime}
<div class="time-divider" /> {#if mode === DateRangeMode.DATETIME}
<div class="time-divider" />
{/if}
<span <span
bind:this={edits[3].el} bind:this={edits[3].el}
class="digit" class="digit"
@ -356,13 +400,12 @@
class="close-btn" class="close-btn"
tabindex="0" tabindex="0"
on:click={() => { on:click={() => {
selected = 'day' selected = defaultSelected
startTyping = true startTyping = true
value = null value = null
edits.forEach((edit) => { setEmptyEdits()
edit.value = -1 const newFocusElement = edits[mode === DateRangeMode.TIME ? 2 : 0].el
}) if (newFocusElement) newFocusElement.focus()
if (edits[0].el) edits[0].el.focus()
}} }}
on:blur={(ev) => unfocus(ev, closeBtn)} on:blur={(ev) => unfocus(ev, closeBtn)}
> >
@ -376,14 +419,18 @@
{#if value !== undefined && value !== null && value.toString() !== ''} {#if value !== undefined && value !== null && value.toString() !== ''}
{#if labelOver !== undefined} {#if labelOver !== undefined}
<Label label={labelOver} /> <Label label={labelOver} />
{:else if value} {:else}
{new Date(value).getDate()} {#if withDate}
{getMonthName(new Date(value), 'short')} {new Date(value).getDate()}
{#if new Date(value).getFullYear() !== today.getFullYear()} {getMonthName(new Date(value), 'short')}
{new Date(value).getFullYear()} {#if new Date(value).getFullYear() !== today.getFullYear()}
{new Date(value).getFullYear()}
{/if}
{/if} {/if}
{#if withTime} {#if withTime}
<div class="time-divider" /> {#if withDate}
<div class="time-divider" />
{/if}
{new Date(value).getHours().toString().padStart(2, '0')} {new Date(value).getHours().toString().padStart(2, '0')}
<span class="separator">:</span> <span class="separator">:</span>
{new Date(value).getMinutes().toString().padStart(2, '0')} {new Date(value).getMinutes().toString().padStart(2, '0')}

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { DateRangeMode } from '@hcengineering/core'
import DatePresenter from './DatePresenter.svelte' import DatePresenter from './DatePresenter.svelte'
export let value: number | null | undefined export let value: number | null | undefined
@ -20,4 +21,4 @@
export let editable: boolean = false export let editable: boolean = false
</script> </script>
<DatePresenter bind:value withTime {mondayStart} {editable} /> <DatePresenter bind:value mode={DateRangeMode.DATETIME} {mondayStart} {editable} />

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { DateRangeMode } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import ui from '../../plugin' import ui from '../../plugin'
import DateRangePresenter from './DateRangePresenter.svelte' import DateRangePresenter from './DateRangePresenter.svelte'
@ -25,4 +26,4 @@
export let noShift: boolean = false export let noShift: boolean = false
</script> </script>
<DateRangePresenter bind:value withTime {editable} {icon} {labelOver} {labelNull} {noShift} /> <DateRangePresenter bind:value mode={DateRangeMode.DATETIME} {editable} {icon} {labelOver} {labelNull} {noShift} />

View File

@ -0,0 +1,17 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="-5 -3 24 24">
<g>
<g id="c98_play">
<path
d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313
c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04
c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z"
/>
</g>
<g id="Capa_1_78_" />
</g>
</svg>

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 16 16">
<rect width="8" height="8" />
</svg>

View File

@ -103,6 +103,8 @@ export { default as Chevron } from './components/Chevron.svelte'
export { default as Timeline } from './components/Timeline.svelte' export { default as Timeline } from './components/Timeline.svelte'
export { default as IconAdd } from './components/icons/Add.svelte' export { default as IconAdd } from './components/icons/Add.svelte'
export { default as IconStart } from './components/icons/Start.svelte'
export { default as IconStop } from './components/icons/Stop.svelte'
export { default as IconBack } from './components/icons/Back.svelte' export { default as IconBack } from './components/icons/Back.svelte'
export { default as IconForward } from './components/icons/Forward.svelte' export { default as IconForward } from './components/icons/Forward.svelte'
export { default as IconClose } from './components/icons/Close.svelte' export { default as IconClose } from './components/icons/Close.svelte'

View File

@ -1,3 +1,4 @@
import { DateRangeMode } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import type { import type {
@ -83,6 +84,7 @@ interface IDatePopup {
anchor: HTMLElement | undefined anchor: HTMLElement | undefined
popup: HTMLElement | undefined popup: HTMLElement | undefined
frendlyFocus: HTMLElement[] | undefined frendlyFocus: HTMLElement[] | undefined
mode?: DateRangeMode
onClose?: (result: any) => void onClose?: (result: any) => void
onChange?: (result: any) => void onChange?: (result: any) => void
shift?: boolean shift?: boolean
@ -96,7 +98,8 @@ export const dpstore = writable<IDatePopup>({
frendlyFocus: undefined, frendlyFocus: undefined,
onClose: undefined, onClose: undefined,
onChange: undefined, onChange: undefined,
shift: undefined shift: undefined,
mode: undefined
}) })
export function showDatePopup ( export function showDatePopup (

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Employee, EmployeeAccount } from '@hcengineering/contact' import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core' import { Class, DateRangeMode, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Card, getClient, UserBoxList } from '@hcengineering/presentation' import { Card, getClient, UserBoxList } from '@hcengineering/presentation'
import ui, { EditBox, DateRangePresenter } from '@hcengineering/ui' import ui, { EditBox, DateRangePresenter } from '@hcengineering/ui'
import { tick } from 'svelte' import { tick } from 'svelte'
@ -101,7 +101,7 @@
value={startDate} value={startDate}
labelNull={ui.string.SelectDate} labelNull={ui.string.SelectDate}
on:change={async (event) => await handleNewStartDate(event.detail)} on:change={async (event) => await handleNewStartDate(event.detail)}
withTime mode={DateRangeMode.DATETIME}
editable editable
/> />
<DateRangePresenter <DateRangePresenter
@ -109,7 +109,7 @@
value={dueDate} value={dueDate}
labelNull={calendar.string.DueTo} labelNull={calendar.string.DueTo}
on:change={async (event) => await handleNewDueDate(event.detail)} on:change={async (event) => await handleNewDueDate(event.detail)}
withTime mode={DateRangeMode.DATETIME}
editable editable
/> />
<UserBoxList bind:items={participants} label={calendar.string.Participants} /> <UserBoxList bind:items={participants} label={calendar.string.Participants} />

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Employee, EmployeeAccount } from '@hcengineering/contact' import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core' import { Class, DateRangeMode, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import { Card, getClient, UserBoxList } from '@hcengineering/presentation' import { Card, getClient, UserBoxList } from '@hcengineering/presentation'
import ui, { EditBox, DateRangePresenter } from '@hcengineering/ui' import ui, { EditBox, DateRangePresenter } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -73,7 +73,7 @@
<EditBox bind:value={title} placeholder={calendar.string.Title} kind={'large-style'} focus /> <EditBox bind:value={title} placeholder={calendar.string.Title} kind={'large-style'} focus />
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">
<!-- <TimeShiftPicker title={calendar.string.Date} bind:value direction="after" /> --> <!-- <TimeShiftPicker title={calendar.string.Date} bind:value direction="after" /> -->
<DateRangePresenter bind:value withTime={true} editable={true} labelNull={ui.string.SelectDate} /> <DateRangePresenter bind:value mode={DateRangeMode.DATETIME} editable={true} labelNull={ui.string.SelectDate} />
<UserBoxList bind:items={participants} label={calendar.string.Participants} /> <UserBoxList bind:items={participants} label={calendar.string.Participants} />
</svelte:fragment> </svelte:fragment>
</Card> </Card>

View File

@ -14,6 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Event } from '@hcengineering/calendar' import { Event } from '@hcengineering/calendar'
import { DateRangeMode } from '@hcengineering/core'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { DateRangePresenter } from '@hcengineering/ui' import { DateRangePresenter } from '@hcengineering/ui'
import calendar from '../plugin' import calendar from '../plugin'
@ -21,10 +22,19 @@
export let value: Event export let value: Event
export let noShift: boolean = false export let noShift: boolean = false
let dateRangeMode: DateRangeMode
$: date = value ? new Date(value.date) : undefined $: date = value ? new Date(value.date) : undefined
$: dueDate = value ? new Date(value.dueDate ?? value.date) : undefined $: dueDate = value ? new Date(value.dueDate ?? value.date) : undefined
$: interval = (value.dueDate ?? value.date) - value.date $: interval = (value.dueDate ?? value.date) - value.date
$: {
if (date && date.getMinutes() !== 0 && date.getHours() !== 0 && interval < DAY) {
dateRangeMode = DateRangeMode.DATETIME
} else {
dateRangeMode = DateRangeMode.DATE
}
}
const SECOND = 1000 const SECOND = 1000
const MINUTE = SECOND * 60 const MINUTE = SECOND * 60
@ -46,11 +56,7 @@
<div class="antiSelect"> <div class="antiSelect">
{#if date} {#if date}
<DateRangePresenter <DateRangePresenter value={date.getTime()} mode={dateRangeMode} {noShift} />
value={date.getTime()}
withTime={date.getMinutes() !== 0 && date.getHours() !== 0 && interval < DAY}
{noShift}
/>
{#if interval > 0} {#if interval > 0}
{#await formatDueDate(interval) then t} {#await formatDueDate(interval) then t}
<span class="ml-2 mr-1 whitespace-nowrap">({t})</span> <span class="ml-2 mr-1 whitespace-nowrap">({t})</span>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Timestamp, TypeDate } from '@hcengineering/core' import { DateRangeMode, Timestamp, TypeDate } from '@hcengineering/core'
import { ticker, tooltip } from '@hcengineering/ui' import { ticker, tooltip } from '@hcengineering/ui'
import { DateEditor } from '@hcengineering/view-resources' import { DateEditor } from '@hcengineering/view-resources'
import EmployeeStatusDueDatePopup from './EmployeeStatusDueDatePopup.svelte' import EmployeeStatusDueDatePopup from './EmployeeStatusDueDatePopup.svelte'
@ -12,7 +12,7 @@
$: formattedDate = statusDueDate && formatDate(statusDueDate) $: formattedDate = statusDueDate && formatDate(statusDueDate)
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const type = { withTime: true } as TypeDate const type = { mode: DateRangeMode.DATETIME, withShift: true } as TypeDate
</script> </script>
<div <div

View File

@ -1,48 +1,51 @@
<!-- <!--
// Copyright © 2022 Hardcore Engineering Inc. // Copyright © 2023 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Ref } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import { Project } from '@hcengineering/tracker'
import { Button, showPopup, eventToHTMLElement } from '@hcengineering/ui' import { Button, showPopup, eventToHTMLElement } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui' import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import contact, { Employee } from '@hcengineering/contact' import contact, { Employee } from '@hcengineering/contact'
import { getClient, UsersPopup } from '@hcengineering/presentation' import { getClient, UsersPopup } from '@hcengineering/presentation'
import { translate } from '@hcengineering/platform' import { IntlString, translate } from '@hcengineering/platform'
import tracker from '../../plugin' import tracker from '../../../tracker-resources/src/plugin'
export let value: Project export let value: Doc
export let kind: ButtonKind = 'no-border' export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small' export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center' export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content' export let width: string | undefined = 'min-content'
export let intlTitle: IntlString
export let intlSearchPh: IntlString
export let retrieveMembers: (doc: Doc) => Ref<Employee>[]
const client = getClient() const client = getClient()
let buttonTitle = '' let buttonTitle = ''
$: translate(tracker.string.ProjectMembersTitle, {}).then((res) => { $: members = retrieveMembers(value)
$: translate(intlTitle, {}).then((res) => {
buttonTitle = res buttonTitle = res
}) })
const handleProjectMembersChanged = async (result: Ref<Employee>[] | undefined) => { const handleMembersChanged = async (result: Ref<Employee>[] | undefined) => {
if (result === undefined) { if (result === undefined) {
return return
} }
const memberToPull = value.members.filter((x) => !result.includes(x))[0] const memberToPull = members.filter((x) => !result.includes(x))[0]
const memberToPush = result.filter((x) => !value.members.includes(x))[0] const memberToPush = result.filter((x) => !members.includes(x))[0]
if (memberToPull) { if (memberToPull) {
await client.update(value, { $pull: { members: memberToPull } }) await client.update(value, { $pull: { members: memberToPull } })
@ -53,22 +56,22 @@
} }
} }
const handleProjectMembersEditorOpened = async (event: MouseEvent) => { const handleMembersEditorOpened = async (event: MouseEvent) => {
showPopup( showPopup(
UsersPopup, UsersPopup,
{ {
_class: contact.class.Employee, _class: contact.class.Employee,
selectedUsers: value.members, selectedUsers: members,
allowDeselect: true, allowDeselect: true,
multiSelect: true, multiSelect: true,
docQuery: { docQuery: {
active: true active: true
}, },
placeholder: tracker.string.ProjectMembersSearchPlaceholder placeholder: intlSearchPh
}, },
eventToHTMLElement(event), eventToHTMLElement(event),
undefined, undefined,
handleProjectMembersChanged handleMembersChanged
) )
} }
</script> </script>
@ -80,5 +83,5 @@
{justify} {justify}
title={buttonTitle} title={buttonTitle}
icon={tracker.icon.ProjectMembers} icon={tracker.icon.ProjectMembers}
on:click={handleProjectMembersEditorOpened} on:click={handleMembersEditorOpened}
/> />

View File

@ -43,6 +43,7 @@ import EmployeeBrowser from './components/EmployeeBrowser.svelte'
import EmployeeEditor from './components/EmployeeEditor.svelte' import EmployeeEditor from './components/EmployeeEditor.svelte'
import EmployeePresenter from './components/EmployeePresenter.svelte' import EmployeePresenter from './components/EmployeePresenter.svelte'
import MemberPresenter from './components/MemberPresenter.svelte' import MemberPresenter from './components/MemberPresenter.svelte'
import MembersPresenter from './components/MembersPresenter.svelte'
import Members from './components/Members.svelte' import Members from './components/Members.svelte'
import OrganizationEditor from './components/OrganizationEditor.svelte' import OrganizationEditor from './components/OrganizationEditor.svelte'
import OrganizationPresenter from './components/OrganizationPresenter.svelte' import OrganizationPresenter from './components/OrganizationPresenter.svelte'
@ -68,7 +69,9 @@ export {
MemberPresenter, MemberPresenter,
EmployeeEditor, EmployeeEditor,
EmployeeAccountRefPresenter, EmployeeAccountRefPresenter,
EditPerson MembersPresenter,
EditPerson,
EmployeeRefPresenter
} }
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({ const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
@ -158,6 +161,7 @@ export default async (): Promise<Resources> => ({
EmployeeRefPresenter, EmployeeRefPresenter,
Members, Members,
MemberPresenter, MemberPresenter,
MembersPresenter,
EditMember, EditMember,
EmployeeArrayEditor, EmployeeArrayEditor,
EmployeeEditor, EmployeeEditor,

View File

@ -208,7 +208,8 @@ const contactPlugin = plugin(contactId, {
SocialEditor: '' as AnyComponent, SocialEditor: '' as AnyComponent,
CreateOrganization: '' as AnyComponent, CreateOrganization: '' as AnyComponent,
CreatePerson: '' as AnyComponent, CreatePerson: '' as AnyComponent,
ChannelsPresenter: '' as AnyComponent ChannelsPresenter: '' as AnyComponent,
MembersPresenter: '' as AnyComponent
}, },
channelProvider: { channelProvider: {
Email: '' as Ref<ChannelProvider>, Email: '' as Ref<ChannelProvider>,

View File

@ -29,7 +29,6 @@
<DateRangePresenter <DateRangePresenter
value={_value} value={_value}
withTime={false}
editable editable
{kind} {kind}
{noShift} {noShift}

View File

@ -16,7 +16,7 @@
import calendar from '@hcengineering/calendar' import calendar from '@hcengineering/calendar'
import type { Contact, EmployeeAccount, Organization, Person } from '@hcengineering/contact' import type { Contact, EmployeeAccount, Organization, Person } from '@hcengineering/contact'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { Account, Class, Client, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core' import { Account, Class, Client, Doc, generateId, getCurrentAccount, Ref, DateRangeMode } from '@hcengineering/core'
import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform' import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform'
import { Card, getClient, UserBox, UserBoxList } from '@hcengineering/presentation' import { Card, getClient, UserBox, UserBoxList } from '@hcengineering/presentation'
import type { Candidate, Review } from '@hcengineering/recruit' import type { Candidate, Review } from '@hcengineering/recruit'
@ -173,11 +173,16 @@
<DateRangePresenter <DateRangePresenter
bind:value={startDate} bind:value={startDate}
labelNull={recruit.string.StartDate} labelNull={recruit.string.StartDate}
withTime mode={DateRangeMode.DATETIME}
editable editable
on:change={updateStart} on:change={updateStart}
/> />
<DateRangePresenter bind:value={dueDate} labelNull={recruit.string.DueDate} withTime editable /> <DateRangePresenter
bind:value={dueDate}
labelNull={recruit.string.DueDate}
mode={DateRangeMode.DATETIME}
editable
/>
<UserBoxList bind:items={doc.participants} label={calendar.string.Participants} /> <UserBoxList bind:items={doc.participants} label={calendar.string.Participants} />
</svelte:fragment> </svelte:fragment>
</Card> </Card>

View File

@ -42,6 +42,7 @@
"Custom": "Custom", "Custom": "Custom",
"Type": "Type", "Type": "Type",
"WithTime": "WithTime", "WithTime": "WithTime",
"DateMode": "Date mode",
"CreatingAttribute": "Creating an attribute", "CreatingAttribute": "Creating an attribute",
"EditAttribute": "Edit attribute", "EditAttribute": "Edit attribute",
"CreateEnum": "Create enum", "CreateEnum": "Create enum",

View File

@ -42,6 +42,7 @@
"Custom": "Пользовательский", "Custom": "Пользовательский",
"Type": "Тип", "Type": "Тип",
"WithTime": "Со временем", "WithTime": "Со временем",
"DateMode": "Тип времени",
"CreatingAttribute": "Создание атрибута", "CreatingAttribute": "Создание атрибута",
"EditAttribute": "Редактирование атрибута", "EditAttribute": "Редактирование атрибута",
"CreateEnum": "Создать справочник", "CreateEnum": "Создать справочник",

View File

@ -13,40 +13,58 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { TypeDate as DateType } from '@hcengineering/core' import { DateRangeMode, TypeDate as DateType } from '@hcengineering/core'
import { TypeDate } from '@hcengineering/model' import { TypeDate } from '@hcengineering/model'
import { Label } from '@hcengineering/ui' import { Label, ListItem } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import setting from '../../plugin' import setting from '../../plugin'
import BooleanEditor from '@hcengineering/view-resources/src/components/BooleanEditor.svelte' import Dropdown from '@hcengineering/ui/src/components/Dropdown.svelte'
import BooleanPresenter from '@hcengineering/view-resources/src/components/BooleanPresenter.svelte' import StringPresenter from '@hcengineering/view-resources/src/components/StringPresenter.svelte'
export let type: DateType | undefined export let type: DateType | undefined
export let editable: boolean = true export let editable: boolean = true
const dispatch = createEventDispatcher()
let withTime: boolean = type?.withTime ?? false const dispatch = createEventDispatcher()
const items: ListItem[] = [
{
_id: DateRangeMode.DATE,
label: DateRangeMode.DATE
},
{
_id: DateRangeMode.TIME,
label: DateRangeMode.TIME
},
{
_id: DateRangeMode.DATETIME,
label: DateRangeMode.DATETIME
}
]
let selected = items.find((item) => item._id === type?.mode)
onMount(() => { onMount(() => {
if (type === undefined) { if (type === undefined) {
dispatch('change', { type: TypeDate(withTime) }) dispatch('change', { type: TypeDate() })
} }
}) })
</script> </script>
<div class="flex-row-center"> <div class="flex-row-center">
<Label label={setting.string.WithTime} /> <Label label={setting.string.DateMode} />
<div class="ml-2"> <div class="ml-2">
{#if editable} {#if editable}
<BooleanEditor <Dropdown
withoutUndefined {selected}
bind:value={withTime} {items}
onChange={(e) => { size="medium"
dispatch('change', { type: TypeDate(e) }) placeholder={setting.string.DateMode}
on:selected={(res) => {
selected = res.detail
dispatch('change', { type: TypeDate(res.detail._id) })
}} }}
/> />
{:else} {:else}
<BooleanPresenter value={withTime} /> <StringPresenter value={selected?.label ?? ''} />
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -37,6 +37,7 @@ export default mergeIds(settingId, setting, {
Attributes: '' as IntlString, Attributes: '' as IntlString,
Custom: '' as IntlString, Custom: '' as IntlString,
WithTime: '' as IntlString, WithTime: '' as IntlString,
DateMode: '' as IntlString,
Type: '' as IntlString, Type: '' as IntlString,
CreatingAttribute: '' as IntlString, CreatingAttribute: '' as IntlString,
EditAttribute: '' as IntlString, EditAttribute: '' as IntlString,

View File

@ -3,6 +3,25 @@
<path d="M17.1,1.2h-4.2C9,1.2,7.2,3,7.2,6.9v0.4H6.9C3,7.2,1.2,9,1.2,12.9v4.2c0,3.9,1.7,5.7,5.6,5.7h4.2 c3.9,0,5.6-1.7,5.6-5.7v-0.3h0.3c3.9,0,5.7-1.7,5.7-5.6V6.9C22.8,3,21,1.2,17.1,1.2z M15.2,17.1c0,3.1-1,4.2-4.1,4.2H6.9 c-3.1,0-4.1-1-4.1-4.2v-4.2c0-3.1,1-4.1,4.1-4.1H8h3.1c3.1,0,4.1,1,4.1,4.1V16V17.1z M21.2,11.1c0,3.1-1,4.1-4.2,4.1h-0.3v-2.4 c0-3.9-1.7-5.6-5.6-5.6H8.8V6.9c0-3.1,1-4.1,4.1-4.1h4.2c3.1,0,4.2,1,4.2,4.1V11.1z" /> <path d="M17.1,1.2h-4.2C9,1.2,7.2,3,7.2,6.9v0.4H6.9C3,7.2,1.2,9,1.2,12.9v4.2c0,3.9,1.7,5.7,5.6,5.7h4.2 c3.9,0,5.6-1.7,5.6-5.7v-0.3h0.3c3.9,0,5.7-1.7,5.7-5.6V6.9C22.8,3,21,1.2,17.1,1.2z M15.2,17.1c0,3.1-1,4.2-4.1,4.2H6.9 c-3.1,0-4.1-1-4.1-4.2v-4.2c0-3.1,1-4.1,4.1-4.1H8h3.1c3.1,0,4.1,1,4.1,4.1V16V17.1z M21.2,11.1c0,3.1-1,4.1-4.2,4.1h-0.3v-2.4 c0-3.9-1.7-5.6-5.6-5.6H8.8V6.9c0-3.1,1-4.1,4.1-4.1h4.2c3.1,0,4.2,1,4.2,4.1V11.1z" />
<path d="M11.4,12.5L8,15.9l-1.4-1.4c-0.3-0.3-0.8-0.3-1.1,0s-0.3,0.8,0,1.1l2,2c0.1,0.1,0.3,0.2,0.5,0.2h0 c0.2,0,0.4-0.1,0.5-0.2l3.9-3.9c0.3-0.3,0.3-0.8,0-1.1C12.2,12.2,11.7,12.2,11.4,12.5z" /> <path d="M11.4,12.5L8,15.9l-1.4-1.4c-0.3-0.3-0.8-0.3-1.1,0s-0.3,0.8,0,1.1l2,2c0.1,0.1,0.3,0.2,0.5,0.2h0 c0.2,0,0.4-0.1,0.5-0.2l3.9-3.9c0.3-0.3,0.3-0.8,0-1.1C12.2,12.2,11.7,12.2,11.4,12.5z" />
</symbol> </symbol>
<symbol id="scrum" viewBox="0 0 16 16">
<g transform="matrix(0.00357, 0, 0, -0.003573, -1.147394, 17.149212)">
<path d="M1001 4784 c-253 -68 -410 -331 -346 -579 45 -170 180 -306 347 -349 l52 -13 -110 -5 c-182 -9 -312 -66 -439 -194 -51 -51 -82 -93 -113 -155 -67 -132 -73 -183 -70 -530 3 -283 4 -299 24 -325 11 -15 33 -37 48 -48 27 -20 40 -21 541 -24 l513 -3 -43 -92 c-78 -167 -113 -310 -122 -492 -16 -344 108 -676 349 -930 l80 -85 -645 -2 c-634 -3 -646 -3 -673 -24 -53 -39 -69 -71 -69 -134 0 -63 16 -95 69 -134 l27 -21 1916 -3 1915 -2 -40 -44 c-68 -74 -69 -161 -3 -227 41 -41 88 -55 149 -44 31 5 67 36 234 203 281 281 281 263 0 544 -167 167 -203 198 -234 203 -61 11 -108 -3 -149 -44 -66 -66 -65 -153 3 -227 l41 -44 -423 0 -422 0 80 85 c135 142 225 293 287 478 102 308 81 642 -60 944 l-43 92 513 3 c501 3 514 4 541 24 15 11 37 33 48 48 20 26 21 40 21 359 l0 332 -27 73 c-38 105 -83 176 -158 252 -123 122 -255 179 -434 188 l-110 5 53 14 c167 42 301 179 346 350 65 249 -94 511 -350 578 -251 65 -513 -94 -580 -350 -64 -249 95 -514 347 -579 l52 -13 -110 -6 c-126 -6 -206 -29 -305 -87 -76 -45 -162 -126 -204 -193 -17 -26 -32 -47 -35 -47 -3 0 -16 19 -30 42 -38 63 -134 154 -208 197 -100 59 -179 82 -306 88 l-110 6 52 13 c252 65 411 330 347 579 -67 256 -329 415 -580 350 -256 -67 -415 -329 -350 -578 45 -171 179 -308 346 -350 l53 -14 -110 -6 c-126 -6 -206 -29 -305 -88 -73 -42 -170 -134 -209 -198 -14 -22 -27 -41 -30 -41 -3 0 -18 20 -33 45 -42 66 -132 151 -206 195 -99 58 -179 81 -305 87 l-110 6 53 14 c251 64 410 330 346 576 -67 259 -333 419 -584 351z m182 -318 c103 -43 128 -177 48 -257 -112 -113 -296 -12 -267 146 18 94 128 150 219 111z m1440 0 c103 -43 128 -177 48 -257 -112 -113 -296 -12 -267 146 18 94 128 150 219 111z m1440 0 c103 -43 128 -177 48 -257 -112 -113 -296 -12 -267 146 18 94 128 150 219 111z m-2653 -975 c76 -40 115 -77 151 -143 l34 -63 3 -202 3 -203 -481 0 -481 0 3 203 c3 188 5 205 27 249 40 82 120 150 211 179 14 4 129 7 255 6 221 -2 232 -3 275 -26z m1440 0 c76 -40 115 -77 150 -143 30 -55 35 -75 38 -152 4 -79 3 -88 -12 -83 -129 50 -328 87 -466 87 -134 0 -341 -38 -464 -86 -18 -7 -18 -2 -14 82 7 135 64 231 170 285 71 37 85 38 323 36 221 -2 232 -3 275 -26z m1440 0 c76 -40 115 -77 151 -143 l34 -63 3 -202 3 -203 -481 0 -481 0 3 203 c3 188 5 205 27 249 40 82 121 150 211 179 14 4 129 7 255 6 221 -2 232 -3 275 -26z m-1499 -640 c179 -46 319 -127 449 -260 129 -130 212 -278 257 -457 24 -95 24 -333 0 -428 -46 -182 -128 -328 -261 -462 -134 -133 -280 -215 -462 -261 -95 -24 -333 -24 -428 0 -182 46 -328 128 -462 261 -133 134 -215 280 -261 462 -24 95 -24 333 0 428 45 179 128 327 257 457 147 150 309 236 513 275 100 19 299 12 398 -15z"/>
<path d="M2980 2549 c-14 -5 -128 -113 -255 -239 l-230 -229 -237 -3 c-221 -3 -238 -4 -264 -24 -53 -39 -69 -71 -69 -134 0 -63 16 -95 69 -134 26 -20 43 -21 304 -24 171 -2 290 1 312 7 26 8 103 79 307 283 292 293 295 297 279 383 -17 91 -127 149 -216 114z"/>
</g>
</symbol>
<symbol id="start" viewBox="-5 -3 24 24">
<g>
<g id="c98_play">
<path d="M2.067,0.043C2.21-0.028,2.372-0.008,2.493,0.085l13.312,8.503c0.094,0.078,0.154,0.191,0.154,0.313
c0,0.12-0.061,0.237-0.154,0.314L2.492,17.717c-0.07,0.057-0.162,0.087-0.25,0.087l-0.176-0.04
c-0.136-0.065-0.222-0.207-0.222-0.361V0.402C1.844,0.25,1.93,0.107,2.067,0.043z"/>
</g>
<g id="Capa_1_78_" />
</g>
</symbol>
<symbol id="stop" viewBox="-4 -4 16 16">
<rect width="8" height="8"/>
</symbol>
<symbol id="project" viewBox="0 0 16 16"> <symbol id="project" viewBox="0 0 16 16">
<path d="M11.7,3.6h-1.1C10.4,2.7,9.7,2,8.8,2H7.2C6.3,2,5.6,2.7,5.4,3.6H4.3C3,3.6,2,4.6,2,5.9V8c0,0.2,0.1,0.3,0.2,0.4 C3.8,9.3,5.9,9.9,8,9.9c2.1,0,4.2-0.5,5.8-1.5C13.9,8.3,14,8.2,14,8V5.9C14,4.6,13,3.6,11.7,3.6z M7.2,3h1.5c0.4,0,0.6,0.2,0.8,0.6 H6.5C6.6,3.2,6.9,3,7.2,3z M13,7.7c-1.4,0.8-3.2,1.2-5,1.2c-1.8,0-3.6-0.4-5-1.2V5.9c0-0.7,0.6-1.3,1.3-1.3h7.4 c0.7,0,1.3,0.6,1.3,1.3V7.7z"/> <path d="M11.7,3.6h-1.1C10.4,2.7,9.7,2,8.8,2H7.2C6.3,2,5.6,2.7,5.4,3.6H4.3C3,3.6,2,4.6,2,5.9V8c0,0.2,0.1,0.3,0.2,0.4 C3.8,9.3,5.9,9.9,8,9.9c2.1,0,4.2-0.5,5.8-1.5C13.9,8.3,14,8.2,14,8V5.9C14,4.6,13,3.6,11.7,3.6z M7.2,3h1.5c0.4,0,0.6,0.2,0.8,0.6 H6.5C6.6,3.2,6.9,3,7.2,3z M13,7.7c-1.4,0.8-3.2,1.2-5,1.2c-1.8,0-3.6-0.4-5-1.2V5.9c0-0.7,0.6-1.3,1.3-1.3h7.4 c0.7,0,1.3,0.6,1.3,1.3V7.7z"/>
<path d="M13.5,9.7c-0.3,0-0.5,0.2-0.5,0.5l-0.1,1.5c-0.1,0.8-0.7,1.3-1.4,1.3H4.5c-0.7,0-1.4-0.6-1.4-1.3L3,10.1 c0-0.3-0.3-0.5-0.5-0.5C2.2,9.7,2,9.9,2,10.2l0.1,1.5C2.2,13,3.3,14,4.5,14h6.9c1.3,0,2.3-1,2.4-2.3l0.1-1.5 C14,9.9,13.8,9.7,13.5,9.7z"/> <path d="M13.5,9.7c-0.3,0-0.5,0.2-0.5,0.5l-0.1,1.5c-0.1,0.8-0.7,1.3-1.4,1.3H4.5c-0.7,0-1.4-0.6-1.4-1.3L3,10.1 c0-0.3-0.3-0.5-0.5-0.5C2.2,9.7,2,9.9,2,10.2l0.1,1.5C2.2,13,3.3,14,4.5,14h6.9c1.3,0,2.3-1,2.4-2.3l0.1-1.5 C14,9.9,13.8,9.7,13.5,9.7z"/>

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -221,6 +221,26 @@
"MoveAndDeleteSprint": "Move Issues to {newSprint} and Delete {deleteSprint}", "MoveAndDeleteSprint": "Move Issues to {newSprint} and Delete {deleteSprint}",
"MoveAndDeleteSprintConfirm": "Do you want to delete sprint and move issues to another sprint?", "MoveAndDeleteSprintConfirm": "Do you want to delete sprint and move issues to another sprint?",
"Scrums": "Scrums",
"Scrum": "Scrum",
"ScrumMembersTitle": "Scrum members",
"ScrumMembersSearchPlaceholder": "Change scrum members\u2026",
"ScrumBeginTime": "Scrum begin time",
"ScrumEndTime": "Scrum end time",
"NewScrum": "New scrum",
"CreateScrum": "Create scrum",
"ScrumTitlePlaceholder": "Scrum title",
"ScrumDescriptionPlaceholder": "Add scrum description",
"ScrumRecords": "Scrum records",
"ScrumRecord": "Scrum record",
"StartRecord": "Start recording",
"StopRecord": "Stop recording",
"ChangeScrumRecord": "Start recording another scrum",
"ChangeScrumRecordConfirm": "Do you want to stop recording {previousRecord} and start recording {newRecord}?",
"ScrumRecorder": "Scrum recorder",
"ScrumRecordTimeReports": "Recorded time reports",
"ScrumRecordObjects": "Changed objects",
"Estimation": "Estimation", "Estimation": "Estimation",
"ReportedTime": "Reported Time", "ReportedTime": "Reported Time",
"TimeSpendReports": "Time spend reports", "TimeSpendReports": "Time spend reports",

View File

@ -221,6 +221,26 @@
"MoveAndDeleteSprint": "Переместить Задачи в {newSprint} и Удалить {deleteSprint}", "MoveAndDeleteSprint": "Переместить Задачи в {newSprint} и Удалить {deleteSprint}",
"MoveAndDeleteSprintConfirm": "Вы действительно хотите удалить спринт и перенести задачи в другой спринт?", "MoveAndDeleteSprintConfirm": "Вы действительно хотите удалить спринт и перенести задачи в другой спринт?",
"Scrums": "Скрамы",
"Scrum": "Скрам",
"ScrumMembersTitle": "Участники скрама",
"ScrumMembersSearchPlaceholder": "Изменить участников скрама\u2026",
"ScrumBeginTime": "Время начала скрама",
"ScrumEndTime": "Время конца скрама",
"NewScrum": "Новый скрам",
"CreateScrum": "Создать скрам",
"ScrumTitlePlaceholder": "Название скрама",
"ScrumDescriptionPlaceholder": "Описание скрама",
"ScrumRecords": "Записи скрамов",
"ScrumRecord": "Запись скрама",
"StartRecord": "Начать запись",
"StopRecord": " Закончить запись",
"ChangeScrumRecord": "Начать запись другого скрама",
"ChangeScrumRecordConfirm": "Вы действительно хотите прекратить запись {previousScrumRecord} и начать записывать {newScrumRecord}?",
"ScrumRecorder": "Ведущий скрама",
"ScrumRecordTimeReports": "Временные отчеты",
"ScrumRecordObjects": "Измененные объекты",
"Estimation": "Оценка", "Estimation": "Оценка",
"ReportedTime": "Использовано", "ReportedTime": "Использовано",
"TimeSpendReports": "Отчеты по времени", "TimeSpendReports": "Отчеты по времени",

View File

@ -36,6 +36,9 @@ loadMetadata(tracker.icon, {
DueDate: `${icons}#inbox`, // TODO: add icon DueDate: `${icons}#inbox`, // TODO: add icon
Parent: `${icons}#myissues`, // TODO: add icon Parent: `${icons}#myissues`, // TODO: add icon
Sprint: `${icons}#sprint`, Sprint: `${icons}#sprint`,
Scrum: `${icons}#scrum`,
Start: `${icons}#start`,
Stop: `${icons}#stop`,
CategoryBacklog: `${icons}#status-backlog`, CategoryBacklog: `${icons}#status-backlog`,
CategoryUnstarted: `${icons}#status-todo`, CategoryUnstarted: `${icons}#status-todo`,

View File

@ -49,6 +49,7 @@
"@hcengineering/notification": "^0.6.5", "@hcengineering/notification": "^0.6.5",
"@hcengineering/notification-resources": "^0.6.0", "@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/contact": "^0.6.9", "@hcengineering/contact": "^0.6.9",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0", "@hcengineering/view-resources": "^0.6.0",
"@hcengineering/text-editor": "^0.6.0", "@hcengineering/text-editor": "^0.6.0",
"@hcengineering/panel": "^0.6.0", "@hcengineering/panel": "^0.6.0",
@ -57,6 +58,8 @@
"@hcengineering/workbench": "^0.6.2", "@hcengineering/workbench": "^0.6.2",
"@hcengineering/attachment": "^0.6.1", "@hcengineering/attachment": "^0.6.1",
"@hcengineering/chunter-resources": "^0.6.0", "@hcengineering/chunter-resources": "^0.6.0",
"@hcengineering/workbench-resources": "^0.6.1" "@hcengineering/workbench-resources": "^0.6.1",
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/activity": "^0.6.0"
} }
} }

View File

@ -22,6 +22,7 @@
export let shouldRender: boolean = true export let shouldRender: boolean = true
export let onDateChange: (newDate: number | null) => void export let onDateChange: (newDate: number | null) => void
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary' export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
export let editable: boolean = true
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0)) $: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
$: isOverdue = dateMs !== null && dateMs < today.getTime() $: isOverdue = dateMs !== null && dateMs < today.getTime()
@ -33,7 +34,7 @@
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => { const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
const newDate = event.detail const newDate = event.detail
if (newDate === undefined || dateMs === newDate) { if (newDate === undefined || dateMs === newDate || !editable) {
return return
} }
@ -58,7 +59,7 @@
> >
<DatePresenter <DatePresenter
value={dateMs} value={dateMs}
editable={true} {editable}
shouldShowLabel={false} shouldShowLabel={false}
icon={iconModifier} icon={iconModifier}
{kind} {kind}
@ -68,7 +69,7 @@
{:else} {:else}
<DatePresenter <DatePresenter
value={dateMs} value={dateMs}
editable={true} {editable}
shouldShowLabel={false} shouldShowLabel={false}
icon={iconModifier} icon={iconModifier}
{kind} {kind}

View File

@ -21,6 +21,7 @@
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary' export let kind: 'transparent' | 'primary' | 'link' | 'list' = 'primary'
export let isEditable = true
const client = getClient() const client = getClient()
@ -48,5 +49,6 @@
dateMs={dueDateMs} dateMs={dueDateMs}
shouldRender={shouldRenderPresenter} shouldRender={shouldRenderPresenter}
onDateChange={handleDueDateChanged} onDateChange={handleDueDateChanged}
editable={isEditable}
{kind} {kind}
/> />

View File

@ -16,12 +16,14 @@
import { Ref, WithLookup } from '@hcengineering/core' import { Ref, WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import type { Issue, Team } from '@hcengineering/tracker' import type { Issue, Team } from '@hcengineering/tracker'
import { showPanel } from '@hcengineering/ui' import { Icon, showPanel, tooltip } from '@hcengineering/ui'
import tracker from '../../plugin' import tracker from '../../plugin'
export let value: WithLookup<Issue> export let value: WithLookup<Issue>
export let disableClick = false export let disableClick = false
export let onClick: (() => void) | undefined = undefined export let onClick: (() => void) | undefined = undefined
export let withIcon = false
export let noUnderline = false
// Extra properties // Extra properties
export let teams: Map<Ref<Team>, Team> | undefined = undefined export let teams: Map<Ref<Team>, Team> | undefined = undefined
@ -56,18 +58,22 @@
{#if value} {#if value}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<span <span class="issuePresenterRoot" class:noPointer={disableClick} class:noUnderline on:click={handleIssueEditorOpened}>
class="issuePresenterRoot" {#if withIcon}
class:noPointer={disableClick} <div class="mr-2" use:tooltip={{ label: tracker.string.Issue }}>
title={value?.title} <Icon icon={tracker.icon.Issues} size={'small'} />
on:click={handleIssueEditorOpened} </div>
> {/if}
{title} <span title={value?.title}>
{title}
</span>
</span> </span>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.issuePresenterRoot { .issuePresenterRoot {
display: flex;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -82,10 +88,18 @@
cursor: default; cursor: default;
} }
&:hover { &.noUnderline {
color: var(--caption-color); color: var(--caption-color);
text-decoration: underline; font-weight: 500;
} }
&:not(.noUnderline) {
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
}
&:active { &:active {
color: var(--accent-color); color: var(--accent-color);
} }

View File

@ -21,8 +21,13 @@
export let value: Issue export let value: Issue
export let shouldUseMargin: boolean = false export let shouldUseMargin: boolean = false
export let showParent = true export let showParent = true
export let onClick: (() => void) | undefined = undefined
function handleIssueEditorOpened () { function handleIssueEditorOpened () {
if (onClick) {
onClick()
}
showPanel(tracker.component.EditIssue, value._id, value._class, 'content') showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
} }
</script> </script>

View File

@ -86,6 +86,8 @@
{ id: 'list', icon: view.icon.List, tooltip: view.string.List }, { id: 'list', icon: view.icon.List, tooltip: view.string.List },
{ id: 'timeline', icon: view.icon.Timeline, tooltip: view.string.Timeline } { id: 'timeline', icon: view.icon.Timeline, tooltip: view.string.Timeline }
] ]
const retrieveMembers = (p: Project) => p.members
</script> </script>
<div class="fs-title flex-between header"> <div class="fs-title flex-between header">
@ -138,7 +140,16 @@
presenter: tracker.component.LeadPresenter, presenter: tracker.component.LeadPresenter,
props: { _class: tracker.class.Project, defaultClass: contact.class.Employee, shouldShowLabel: false } props: { _class: tracker.class.Project, defaultClass: contact.class.Employee, shouldShowLabel: false }
}, },
{ key: '', presenter: tracker.component.ProjectMembersPresenter, props: { kind: 'link' } }, {
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ProjectMembersTitle,
intlSearchPh: tracker.string.ProjectMembersSearchPlaceholder,
retrieveMembers
}
},
{ key: '', presenter: tracker.component.TargetDatePresenter }, { key: '', presenter: tracker.component.TargetDatePresenter },
{ key: '', presenter: tracker.component.ProjectStatusPresenter } { key: '', presenter: tracker.component.ProjectStatusPresenter }
]} ]}

View File

@ -15,11 +15,20 @@
<script lang="ts"> <script lang="ts">
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import { Project } from '@hcengineering/tracker' import { Project } from '@hcengineering/tracker'
import { getCurrentLocation, navigate } from '@hcengineering/ui' import { getCurrentLocation, Icon, navigate, tooltip } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: WithLookup<Project> export let value: WithLookup<Project>
export let withIcon = false
export let onClick: () => void | undefined
function navigateToProject () { function navigateToProject () {
if (onClick) {
onClick()
}
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path[4] = 'projects'
loc.path[5] = value._id loc.path[5] = value._id
loc.path.length = 6 loc.path.length = 6
navigate(loc) navigate(loc)
@ -28,11 +37,14 @@
{#if value} {#if value}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<span <div class="flex" on:click={navigateToProject}>
title={value.label} {#if withIcon}
class="cursor-pointer fs-bold caption-color overflow-label clear-mins" <div class="mr-2" use:tooltip={{ label: tracker.string.Project }}>
on:click={navigateToProject} <Icon icon={tracker.icon.Projects} size={'small'} />
> </div>
{value.label} {/if}
</span> <span title={value.label} class="fs-bold cursor-pointer caption-color overflow-label clear-mins">
{value.label}
</span>
</div>
{/if} {/if}

View File

@ -0,0 +1,41 @@
<!--
// 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 { Doc, TxCUD } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { AttributeModel } from '@hcengineering/view'
export let value: TxCUD<Doc>
export let onNavigate: () => void | undefined
const query = createQuery()
const client = getClient()
let presenter: AttributeModel | undefined
let doc: Doc | undefined
$: query.query(value.objectClass, { _id: value.objectId }, (res) => {
doc = res.shift()
})
$: getObjectPresenter(client, value.objectClass, { key: '' }).then((p) => {
presenter = p
})
</script>
{#if doc && presenter}
<svelte:component this={presenter.presenter} value={doc} onClick={onNavigate} withIcon noUnderline />
{/if}

View File

@ -0,0 +1,102 @@
<!--
// Copyright © 2023 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, DateRangeMode, generateId, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, getClient, SpaceSelector, UserBoxList } from '@hcengineering/presentation'
import { Scrum, Team } from '@hcengineering/tracker'
import { DateRangePresenter, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { StyledTextArea } from '@hcengineering/text-editor'
export let space: Ref<Team>
const objectId: Ref<Scrum> = generateId()
const dispatch = createEventDispatcher()
const client = getClient()
const object: Partial<Data<Scrum>> = {
title: '' as IntlString,
description: '',
members: [],
attachments: 0,
scrumRecords: 0
}
let canSave = false
async function onSave () {
if (object.beginTime && object.endTime) {
await client.createDoc(tracker.class.Scrum, space, object as Data<Scrum>, objectId)
}
}
$: {
if (
object.endTime &&
object.beginTime &&
object.endTime - object.beginTime > 0 &&
object.title !== '' &&
object.members?.length !== 0
) {
canSave = true
} else {
canSave = false
}
}
</script>
<Card
label={tracker.string.NewScrum}
okLabel={tracker.string.CreateScrum}
{canSave}
okAction={onSave}
on:close={() => dispatch('close')}
>
<svelte:fragment slot="header">
<SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space />
</svelte:fragment>
<EditBox bind:value={object.title} placeholder={tracker.string.ScrumTitlePlaceholder} kind={'large-style'} focus />
<StyledTextArea
bind:content={object.description}
placeholder={tracker.string.ScrumDescriptionPlaceholder}
emphasized
/>
<svelte:fragment slot="pool">
<UserBoxList bind:items={object.members} label={tracker.string.ScrumMembersSearchPlaceholder} />
<DateRangePresenter
value={object.beginTime}
labelNull={tracker.string.ScrumBeginTime}
mode={DateRangeMode.TIME}
on:change={(res) => {
if (res.detail !== undefined && res.detail !== null) {
object.beginTime = res.detail
}
}}
editable
/>
<DateRangePresenter
value={object.endTime}
labelNull={tracker.string.ScrumEndTime}
mode={DateRangeMode.TIME}
on:change={(res) => {
if (res.detail !== undefined && res.detail !== null) {
object.endTime = res.detail
}
}}
editable
/>
</svelte:fragment>
</Card>

View File

@ -0,0 +1,22 @@
<script lang="ts">
import { getClient } from '@hcengineering/presentation'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { Button, IconStart, IconStop } from '@hcengineering/ui'
import { handleRecordingScrum } from '../..'
import tracker from '../../plugin'
export let scrum: Scrum
export let activeScrumRecord: ScrumRecord | undefined
const client = getClient()
$: isRecording = scrum._id === activeScrumRecord?.attachedTo
</script>
<Button
size="small"
icon={isRecording ? IconStop : IconStart}
label={isRecording ? tracker.string.StopRecord : tracker.string.StartRecord}
kind={'primary'}
on:click={() => handleRecordingScrum(client, scrum, activeScrumRecord)}
/>

View File

@ -0,0 +1,38 @@
<!--
// Copyright © 2023 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 { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { Button } from '@hcengineering/ui'
import { handleRecordingScrum } from '../..'
import tracker from '../../plugin'
export let value: Scrum
export let activeScrumRecord: ScrumRecord | undefined
let title: string
const client = getClient()
$: isRecording = value._id === activeScrumRecord?.attachedTo
$: translate(isRecording ? tracker.string.StopRecord : tracker.string.StartRecord, {}).then((res) => (title = res))
</script>
<Button
kind="link"
justify="center"
{title}
icon={isRecording ? tracker.icon.Stop : tracker.icon.Start}
on:click={async () => handleRecordingScrum(client, value, activeScrumRecord)}
/>

View File

@ -0,0 +1,59 @@
<!--
// Copyright © 2023 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 { DateRangeMode } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Scrum } from '@hcengineering/tracker'
import { DateRangePresenter } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: Scrum
const client = getClient()
$: beginTime = value.beginTime
$: endTime = value.endTime
const updateObject = (fields: Partial<Scrum> | undefined) => {
if (fields) {
client.update(value, fields)
}
}
</script>
<DateRangePresenter
value={beginTime}
mode={DateRangeMode.TIME}
labelNull={tracker.string.ScrumBeginTime}
on:change={(res) => {
if (res.detail !== null && res.detail !== undefined && res.detail < endTime) {
updateObject({ beginTime: res.detail })
}
}}
noShift
editable
/>
<DateRangePresenter
value={endTime}
mode={DateRangeMode.TIME}
labelNull={tracker.string.ScrumEndTime}
on:change={(res) => {
if (res.detail !== null && res.detail !== undefined && res.detail > beginTime) {
updateObject({ endTime: res.detail })
}
}}
noShift
editable
/>

View File

@ -0,0 +1,48 @@
<!--
// Copyright © 2023 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 '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import type { Scrum } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
import { DocAttributeBar } from '@hcengineering/view-resources'
import tracker from '../../plugin'
export let scrum: Scrum
const client = getClient()
async function change (field: string, value: any) {
await client.update(scrum, { [field]: value })
}
</script>
<div class="popupPanel-body__aside flex shown">
<div class="p-4 w-60 left-divider">
<div class="fs-title text-xl">
<EditBox bind:value={scrum.title} on:change={() => scrum.title && change('title', scrum.title)} />
</div>
<div class="mt-2">
<StyledTextBox
alwaysEdit
showButtons={false}
placeholder={tracker.string.Description}
content={scrum.description ?? ''}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<DocAttributeBar object={scrum} ignoreKeys={['title', 'description']} />
</div>
</div>

View File

@ -0,0 +1,71 @@
<!--
// Copyright © 2023 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 {
Button,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
Icon,
navigate,
showPopup
} from '@hcengineering/ui'
import tracker from '../../plugin'
import ScrumPopup from './ScrumPopup.svelte'
import Expanded from '../icons/Expanded.svelte'
import { SortingOrder, WithLookup } from '@hcengineering/core'
import { Scrum } from '@hcengineering/tracker'
export let scrum: WithLookup<Scrum>
let container: HTMLElement
$: twoRows = $deviceInfo.twoRows
const handleSelectScrum = (evt: MouseEvent): void => {
showPopup(
ScrumPopup,
{
_class: tracker.class.Scrum,
query: { space: scrum.space },
options: { sort: { beginTime: SortingOrder.Ascending } }
},
container,
(value) => {
if (value != null) {
const loc = getCurrentLocation()
loc.path[5] = value._id
navigate(loc)
}
}
)
}
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
<div class:ac-header-full={!twoRows} class:flex-between={twoRows}>
<div bind:this={container} class="ac-header__wrap-title mr-3">
<Button size={'small'} kind={'link'} on:click={handleSelectScrum}>
<svelte:fragment slot="content">
<div class="ac-header__icon">
<Icon icon={tracker.icon.Scrum} size={'small'} />
</div>
<span class="ac-header__title mr-1">{scrum.title}</span>
<Icon icon={Expanded} size={'small'} />
</svelte:fragment>
</Button>
</div>
</div>
<slot name="options" />
</div>

View File

@ -0,0 +1,43 @@
<!--
// Copyright © 2023 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 { DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
import { ObjectPopup } from '@hcengineering/presentation'
import { Scrum } from '@hcengineering/tracker'
import ScrumTitle from './ScrumTitle.svelte'
import tracker from '../../plugin'
export let selected: Ref<Scrum> | undefined = undefined
export let query: DocumentQuery<Scrum> = {}
export let options: FindOptions<Scrum> = {}
</script>
<ObjectPopup
_class={tracker.class.Scrum}
{selected}
bind:docQuery={query}
{options}
searchField="title"
multiSelect={false}
allowDeselect={false}
closeAfterSelect
shadows
on:update
on:close
>
<svelte:fragment slot="item" let:item={scrum}>
<ScrumTitle value={scrum} />
</svelte:fragment>
</ObjectPopup>

View File

@ -0,0 +1,44 @@
<!--
// Copyright © 2023 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 { Scrum } from '@hcengineering/tracker'
import { getCurrentLocation, navigate } from '@hcengineering/ui'
export let value: Scrum
function navigateToScrum () {
const loc = getCurrentLocation()
loc.path[5] = value._id
loc.path.length = 6
navigate(loc)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={navigateToScrum}>
<span title={value.title} class="scrumLabel flex-grow">{value.title}</span>
</div>
<style lang="scss">
.scrumLabel {
display: block;
min-width: 0;
font-weight: 500;
text-align: left;
color: var(--theme-caption-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,54 @@
<!--
// Copyright © 2023 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 { Button, deviceOptionsStore as deviceInfo, Icon, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import Expanded from '../icons/Expanded.svelte'
import { WithLookup } from '@hcengineering/core'
import { ScrumRecord } from '@hcengineering/tracker'
import ScrumRecordPopup from './ScrumRecordPopup.svelte'
import ScrumRecordTitlePresenter from './ScrumRecordTitlePresenter.svelte'
export let scrumRecord: WithLookup<ScrumRecord>
let container: HTMLElement
const dispatch = createEventDispatcher()
$: twoRows = $deviceInfo.twoRows
const handleSelectScrumRecord = (evt: MouseEvent): void => {
showPopup(ScrumRecordPopup, { query: { attachedTo: scrumRecord.attachedTo } }, container, (value) => {
if (value != null) {
scrumRecord = value
dispatch('scrumRecord', scrumRecord._id)
}
})
}
</script>
<div class="ac-header withSettings" class:full={!twoRows} class:mini={twoRows}>
<div class:ac-header-full={!twoRows} class:flex-between={twoRows}>
<div bind:this={container} class="ac-header__wrap-title mr-3">
<Button size={'small'} kind={'link'} on:click={handleSelectScrumRecord}>
<svelte:fragment slot="content">
<ScrumRecordTitlePresenter value={scrumRecord} />
<Icon icon={Expanded} size={'small'} />
</svelte:fragment>
</Button>
</div>
</div>
<slot name="options" />
</div>

View File

@ -0,0 +1,68 @@
<!--
// Copyright © 2023 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 { DateRangeMode, WithLookup } from '@hcengineering/core'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { DateRangePresenter, Label } from '@hcengineering/ui'
import tracker from '../../plugin'
import { EmployeeRefPresenter } from '@hcengineering/contact-resources'
export let scrumRecord: WithLookup<ScrumRecord>
export let scrum: Scrum
</script>
<div class="content">
<span class="label fs-bold">
<Label label={tracker.string.ScrumRecorder} />
</span>
<EmployeeRefPresenter value={scrumRecord.$lookup?.scrumRecorder?.employee} />
<span class="label fs-bold">
<Label label={tracker.string.ScrumBeginTime} />
</span>
<DateRangePresenter value={scrumRecord.startTs} mode={DateRangeMode.DATETIME} kind="link" editable={false} />
{#if scrumRecord.endTs}
<span class="label fs-bold">
<Label label={tracker.string.ScrumEndTime} />
</span>
<DateRangePresenter value={scrumRecord.endTs} mode={DateRangeMode.DATETIME} kind="link" editable={false} />
{/if}
<span class="label fs-bold">
<Label label={tracker.string.Scrum} />
</span>
<span class="fs-bold scrumTitle">
{scrum.title}
</span>
</div>
<style lang="scss">
.content {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-flow: row;
justify-content: start;
align-items: center;
gap: 1rem;
margin-top: 1rem;
width: 100%;
height: min-content;
}
.scrumTitle {
color: var(--accent-color);
}
</style>

View File

@ -0,0 +1,114 @@
<!--
// Copyright © 2023 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 core, { AttachedDoc, Doc, SortingOrder, TxCollectionCUD, TxCUD } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import tracker from '../../plugin'
import { List } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { groupBy } from '@hcengineering/view-resources/src/utils'
import ChangedObjectPresenter from './ChangedObjectPresenter.svelte'
export let txes: TxCUD<Doc>[]
export let onNavigate: () => void
const TRACKED_OBJECTS = [
tracker.class.Issue,
tracker.class.IssueTemplate,
tracker.class.Project,
tracker.class.Sprint
] as const
let changedObjectTxes: TxCUD<Doc>[] = []
// Drop RemoveDoc Txes and filter by supported tracked objects
$: filteredTxesCU = txes
.filter((tx) => TRACKED_OBJECTS.includes(tx.objectClass))
.filter((tx) => {
if (tx.objectClass === tracker.class.Issue) {
const issueTx = tx as TxCollectionCUD<Issue, AttachedDoc>
return issueTx.tx.objectClass !== tracker.class.Issue || issueTx.tx._class !== core.class.TxRemoveDoc
}
return tx._class !== core.class.TxRemoveDoc
})
// Convert Issue txes to common model
$: objectTxes = filteredTxesCU.map((tx) => {
const objectTx = { ...tx }
if (tx.objectClass === tracker.class.Issue) {
const issueTx = tx as TxCollectionCUD<Issue, AttachedDoc>
if (issueTx.tx.objectClass === tracker.class.Issue) {
objectTx.objectId = issueTx.tx.objectId
objectTx._class = issueTx.tx._class
} else {
objectTx._class = core.class.TxUpdateDoc
}
}
return objectTx
})
// Retrieve single txes for changed objects: TxCreateDoc if it exist for object, else last TxUpdateDoc
$: {
changedObjectTxes = []
const txesByObjectId = groupBy(objectTxes, 'objectId')
Object.values(txesByObjectId).forEach((objectTxes) => {
let objectTx: TxCUD<Doc> | undefined
objectTxes.forEach((tx) => {
const currentTx = tx as TxCUD<Doc>
if (
!objectTx ||
currentTx._class === core.class.TxCreateDoc ||
(objectTx._class === core.class.TxUpdateDoc && objectTx.modifiedOn < currentTx.modifiedOn)
) {
objectTx = currentTx
}
})
if (objectTx) {
changedObjectTxes.push(objectTx)
}
})
changedObjectTxes = changedObjectTxes.sort((leftObjectTx, rightObjectTx) =>
leftObjectTx.objectClass.localeCompare(rightObjectTx.objectClass)
)
}
</script>
<List
_class={core.class.TxCUD}
documents={changedObjectTxes}
config={[
{
key: '',
presenter: ChangedObjectPresenter,
props: { onNavigate }
},
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: {}
}
]}
viewOptions={{ orderBy: ['modifiedOn', SortingOrder.Descending], groupBy: [] }}
disableHeader
/>

View File

@ -0,0 +1,149 @@
<!--
// Copyright © 2023 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 core, { Class, Doc, Ref, SortingOrder, TxCUD, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { UpDownNavigator } from '@hcengineering/view-resources'
import { Panel } from '@hcengineering/panel'
import { Button, closePanel, TabItem, TabList } from '@hcengineering/ui'
import tracker from '../../plugin'
import { handleRecordingScrum } from '../..'
import ScrumRecordInfo from './ScrumRecordInfo.svelte'
import contact from '@hcengineering/contact'
import ScrumRecordTimeSpend from './ScrumRecordTimeSpend.svelte'
import ScrumRecordObjects from './ScrumRecordObjects.svelte'
import { scrumRecordTitleMap, ScrumRecordViewMode } from '../../utils'
export let _id: Ref<ScrumRecord>
export let _class: Ref<Class<ScrumRecord>>
const scrumRecordQuery = createQuery()
const client = getClient()
const txQuery = createQuery()
const modeList: TabItem[] = Object.entries(scrumRecordTitleMap).map(([id, labelIntl]) => ({
id,
labelIntl,
action: () => handleViewModeChanged(id as ScrumRecordViewMode)
}))
let scrumRecord: WithLookup<ScrumRecord> | undefined
let scrum: Scrum | undefined
let isRecording = false
let txes: TxCUD<Doc>[] = []
let mode: ScrumRecordViewMode = 'timeReports'
const onNavigate = () => closePanel()
const handleViewModeChanged = (newMode: ScrumRecordViewMode) => {
if (newMode === undefined || newMode === mode) {
return
}
mode = newMode
}
$: _class &&
_id &&
scrumRecordQuery.query(
_class,
{ _id },
(result) => {
scrumRecord = result.shift()
},
{
lookup: {
attachedTo: tracker.class.Scrum,
scrumRecorder: contact.class.EmployeeAccount
}
}
)
$: scrum = scrumRecord?.$lookup?.attachedTo
$: {
if (scrumRecord?.startTs && !scrumRecord.endTs && scrumRecord.scrumRecorder) {
isRecording = true
} else {
isRecording = false
}
}
$: scrumRecord &&
txQuery.query(
core.class.TxCUD,
{
modifiedOn: { $gte: scrumRecord.startTs, ...(scrumRecord.endTs ? { $lte: scrumRecord.endTs } : {}) },
modifiedBy: scrumRecord.scrumRecorder
},
(res) => {
txes = res
},
{ sort: { modifiedOn: SortingOrder.Ascending } }
)
</script>
{#if scrumRecord && scrum}
<Panel object={scrumRecord} isUtils={isRecording} isHeader={false} on:close>
<svelte:fragment slot="navigator">
<UpDownNavigator element={scrumRecord} />
</svelte:fragment>
<svelte:fragment slot="title">
<span class="fs-title select-text-i">
{scrumRecord.label}
</span>
</svelte:fragment>
<svelte:fragment slot="utils">
{#if isRecording}
<Button
kind="transparent"
showTooltip={{ label: tracker.string.StopRecord }}
icon={tracker.icon.Stop}
on:click={() => scrum && handleRecordingScrum(client, scrum, scrumRecord)}
/>
{/if}
</svelte:fragment>
<svelte:fragment slot="custom-attributes">
<ScrumRecordInfo {scrumRecord} {scrum} />
</svelte:fragment>
<div class="itemsContainer">
<div class="flex-row-center">
<TabList
items={modeList}
selected={mode}
on:select={(result) => {
if (result.detail !== undefined && result.detail.action) result.detail.action()
}}
/>
</div>
</div>
{#if mode === 'timeReports'}
<ScrumRecordTimeSpend {txes} members={scrum.members} {onNavigate} />
{/if}
{#if mode === 'objects'}
<ScrumRecordObjects {txes} {onNavigate} />
{/if}
</Panel>
{/if}
<style lang="scss">
.itemsContainer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 0.75rem 0.65rem 2.25rem;
}
</style>

View File

@ -0,0 +1,43 @@
<!--
// Copyright © 2023 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 { ScrumRecord } from '@hcengineering/tracker'
import { showPanel } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: ScrumRecord
function handleOpenPanel () {
showPanel(tracker.component.ScrumRecordPanel, value._id, value._class, 'content')
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={handleOpenPanel}>
<span title={value.label} class="scrumRecordLabel flex-grow">{value.label}</span>
</div>
<style lang="scss">
.scrumRecordLabel {
display: block;
min-width: 0;
font-weight: 500;
text-align: left;
color: var(--theme-caption-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@ -0,0 +1,194 @@
<!--
// Copyright © 2023 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 { createQuery } from '@hcengineering/presentation'
import core, { Doc, Ref, SortingOrder, TxCollectionCUD, TxCreateDoc, TxCUD, TxUpdateDoc } from '@hcengineering/core'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import tracker from '../../plugin'
import { Employee } from '@hcengineering/contact'
import view from '@hcengineering/view'
import { List } from '@hcengineering/view-resources'
type TimeSpendByEmployee = { [key: Ref<Employee>]: number | undefined }
type TimeSpendByIssue = { [key: Ref<Issue>]: TimeSpendByEmployee | undefined }
type TimeSpendInfo = {
issueId: Ref<Issue>
value: number
employee: Ref<Employee>
}
export let members: Ref<Employee>[]
export let txes: TxCUD<Doc>[] = []
export let onNavigate: () => void
const issuesQuery = createQuery()
let timeSpendInfoByIssue: TimeSpendByIssue = {}
let viewableIssues: Issue[] = []
$: issueTxes = txes.filter((tx) => tx.objectClass === tracker.class.Issue)
$: {
timeSpendInfoByIssue = {}
const timeSpendRecords: { [key: Ref<TimeSpendReport>]: TimeSpendInfo | undefined } = {}
const timeSpendTxes = (issueTxes as TxCollectionCUD<Issue, TimeSpendReport>[]).filter(
(tx) => tx.tx.objectClass === tracker.class.TimeSpendReport
)
const addNewTimeSpend = (
issueId: Ref<Issue>,
timeSpendId: Ref<TimeSpendReport>,
employee?: Ref<Employee> | null,
newValue?: number
) => {
if (newValue && employee && members.includes(employee)) {
if (!(issueId in timeSpendInfoByIssue)) {
timeSpendInfoByIssue[issueId] = {}
}
const recordedValue = timeSpendInfoByIssue[issueId]![employee] ?? 0
timeSpendInfoByIssue[issueId]![employee] = newValue + recordedValue
timeSpendRecords[timeSpendId] = {
issueId,
employee,
value: newValue
}
}
}
timeSpendTxes
.filter((tx) => tx.tx._class === core.class.TxCreateDoc)
.forEach((tx) => {
const timeSpendTxCreate = tx.tx as TxCreateDoc<TimeSpendReport>
const employee = timeSpendTxCreate.attributes.employee
const newValue = timeSpendTxCreate.attributes.value
addNewTimeSpend(tx.objectId, tx.tx.objectId, employee, newValue)
console.log(JSON.stringify({ newValue, employee }))
console.log('TX:', JSON.stringify(tx.tx))
})
timeSpendTxes
.filter((tx) => tx.tx._class === core.class.TxUpdateDoc)
.forEach((tx) => {
const timeSpendTxUpdate = tx.tx as TxUpdateDoc<TimeSpendReport>
const employee = timeSpendTxUpdate.operations.employee
const value = timeSpendTxUpdate.operations.value
const timeSpendId = timeSpendTxUpdate.objectId
const recordedTimeSpend = timeSpendRecords[timeSpendId]
if (employee || value) {
if (recordedTimeSpend) {
const recordedValueByEmployee =
timeSpendInfoByIssue[recordedTimeSpend.issueId]![recordedTimeSpend.employee]!
const newValue = recordedValueByEmployee - recordedTimeSpend.value
if (newValue === 0) {
delete timeSpendInfoByIssue[recordedTimeSpend.issueId]![recordedTimeSpend.employee]
} else {
timeSpendInfoByIssue[recordedTimeSpend.issueId]![recordedTimeSpend.employee] = newValue
}
}
const recordingValue = value ?? recordedTimeSpend?.value
const recordingEmployee = employee ?? recordedTimeSpend?.employee
console.log(JSON.stringify({ recordingValue, recordingEmployee }))
console.log('TX:', JSON.stringify(tx.tx))
addNewTimeSpend(tx.objectId, timeSpendId, recordingEmployee, recordingValue)
}
})
}
// Update reported time and assignee for tracked issues according to tracked TimeSpendReports
$: issuesQuery.query(
tracker.class.Issue,
{
_id: { $in: Object.keys(timeSpendInfoByIssue) as Ref<Issue>[] }
},
(res) => {
const issues = res
viewableIssues = []
for (const [issueId, timeSpendInfo] of Object.entries(timeSpendInfoByIssue)) {
const currentIssue = issues.find((issue) => issue._id === issueId)
if (!timeSpendInfo || !currentIssue) {
return
}
for (const [employeeId, reportedTime] of Object.entries(timeSpendInfo)) {
viewableIssues.push({ ...currentIssue, reportedTime: reportedTime!, assignee: employeeId as Ref<Employee> })
}
viewableIssues = viewableIssues.sort(
(issueLeft, issueRight) => issueRight.reportedTime - issueLeft.reportedTime
)
}
},
{
sort: { priority: SortingOrder.Ascending }
}
)
</script>
<List
_class={tracker.class.Issue}
documents={viewableIssues}
config={[
{
key: '',
presenter: tracker.component.PriorityEditor,
props: { kind: 'list', size: 'small', isEditable: false }
},
{ key: '', presenter: tracker.component.IssuePresenter, props: { onClick: onNavigate } },
{
key: '',
presenter: tracker.component.StatusEditor,
props: { kind: 'list', size: 'small', justify: 'center', isEditable: false }
},
{
key: '',
presenter: tracker.component.TitlePresenter,
props: { shouldUseMargin: true, showParent: false, onClick: onNavigate }
},
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ key: '', presenter: view.component.GrowPresenter, props: { type: 'grow' } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list', isEditable: false } },
{
key: '',
presenter: tracker.component.SprintEditor,
props: {
kind: 'list',
size: 'small',
shape: 'round',
shouldShowPlaceholder: false,
excludeByKey: 'sprint',
isEditable: false
}
},
{
key: '',
presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small', isEditable: false }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
props: {}
}
]}
viewOptions={{ orderBy: ['modifiedOn', SortingOrder.Descending], groupBy: ['assignee'] }}
/>

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2023 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 { SortingOrder } from '@hcengineering/core'
import { Scrum, ScrumRecord } from '@hcengineering/tracker'
import { Button, Icon, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
import { ActionContext, List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import RecordScrumButton from './RecordScrumButton.svelte'
import ScrumEditor from './ScrumEditor.svelte'
import ScrumHeader from './ScrumHeader.svelte'
import ScrumRecordPresenter from './ScrumRecordPresenter.svelte'
export let scrum: Scrum
export let activeScrumRecord: ScrumRecord | undefined
let asideShown = true
$: query = { space: scrum.space, attachedTo: scrum._id }
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<ScrumHeader {scrum}>
<svelte:fragment slot="options">
<RecordScrumButton {scrum} {activeScrumRecord} />
<Button
icon={asideShown ? IconDetailsFilled : IconDetails}
kind={'transparent'}
size={'medium'}
selected={asideShown}
on:click={() => (asideShown = !asideShown)}
/>
</svelte:fragment>
</ScrumHeader>
<div class="top-divider flex w-full h-full clear-mins">
<List
_class={tracker.class.ScrumRecord}
space={scrum.space}
{query}
viewOptions={{
orderBy: ['modifiedOn', SortingOrder.Descending],
groupBy: []
}}
config={[
{ key: '', presenter: Icon, props: { icon: tracker.icon.Scrum, size: 'small' } },
{ key: '', presenter: ScrumRecordPresenter },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter
}
]}
disableHeader
/>
{#if asideShown}
<ScrumEditor bind:scrum />
{/if}
</div>

View File

@ -0,0 +1,41 @@
<!--
// Copyright © 2023 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 { Scrum } from '@hcengineering/tracker'
import { Icon } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: Scrum | undefined
const getMinutes = (date: Date) => {
const currentMinutes = date.getMinutes()
return Math.floor(currentMinutes / 10) > 0 ? currentMinutes : `0${currentMinutes}`
}
</script>
{#if value}
{@const start = new Date(value.beginTime)}
{@const end = new Date(value.endTime)}
<span class="overflow-label flex-row-center flex-grow">
<Icon icon={tracker.icon.Scrum} size={'small'} />
<div class="ml-2 mr-2">
{value.title}
</div>
<span class="flex flex-grow justify-end">
{`${start.getHours()}:${getMinutes(start)}`}
-
{`${end.getHours()}:${getMinutes(end)}`}
</span>
</span>
{/if}

View File

@ -0,0 +1,70 @@
<!--
// Copyright © 2023 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 '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Scrum, ScrumRecord, Team } from '@hcengineering/tracker'
import { closePopup, closeTooltip, location } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../../plugin'
import ScrumRecordsView from './ScrumRecordsView.svelte'
import ScrumsView from './ScrumsView.svelte'
export let currentSpace: Ref<Team>
let scrumId: Ref<Scrum> | undefined
let scrum: Scrum | undefined
let activeScrumRecord: ScrumRecord | undefined
const activeRecordQuery = createQuery()
const scrumQuery = createQuery()
onDestroy(
location.subscribe(async (loc) => {
closeTooltip()
closePopup()
scrumId = loc.path[5] as Ref<Scrum>
})
)
$: if (scrumId) {
scrumQuery.query(tracker.class.Scrum, { _id: scrumId }, (res) => {
scrum = res.shift()
})
} else {
scrumQuery.unsubscribe()
scrum = undefined
}
$: activeRecordQuery.query(
tracker.class.ScrumRecord,
{
space: currentSpace,
scrumRecorder: { $exists: true },
startTs: { $exists: true },
endTs: { $exists: false }
},
(result) => {
activeScrumRecord = result.shift()
}
)
</script>
{#if scrum}
<ScrumRecordsView {activeScrumRecord} {scrum} />
{:else}
<ScrumsView {activeScrumRecord} {currentSpace} />
{/if}

View File

@ -0,0 +1,81 @@
<!--
// Copyright © 2023 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 '@hcengineering/contact'
import { Ref, SortingOrder } from '@hcengineering/core'
import { ScrumRecord, Sprint, Team } from '@hcengineering/tracker'
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import { ActionContext, List } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import NewScrum from './NewScrum.svelte'
import RecordScrumPresenter from './RecordScrumPresenter.svelte'
import ScrumDatePresenter from './ScrumDatePresenter.svelte'
import ScrumPresenter from './ScrumPresenter.svelte'
export let currentSpace: Ref<Team>
export let activeScrumRecord: ScrumRecord | undefined
const showCreateDialog = async () => {
showPopup(NewScrum, { space: currentSpace, targetElement: null }, null)
}
const retrieveMembers = (s: Sprint) => s.members
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
<div class="fs-title flex-between header">
<Label label={tracker.string.Scrums} />
<div class="flex-between flex-gap-2">
<Button size="small" icon={IconAdd} label={tracker.string.Scrum} kind={'primary'} on:click={showCreateDialog} />
</div>
</div>
<List
_class={tracker.class.Scrum}
query={{ space: currentSpace }}
space={currentSpace}
config={[
{ key: '', presenter: Icon, props: { icon: tracker.icon.Scrum, size: 'small' } },
{ key: '', presenter: ScrumPresenter, props: { kind: 'list' } },
{
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.ScrumMembersTitle,
intlSearchPh: tracker.string.ScrumMembersSearchPlaceholder,
retrieveMembers
}
},
{ key: '', presenter: ScrumDatePresenter },
{ key: '', presenter: RecordScrumPresenter, props: { activeScrumRecord } },
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter
}
]}
viewOptions={{ orderBy: ['beginTime', SortingOrder.Ascending], groupBy: [] }}
disableHeader
/>
<style lang="scss">
.header {
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
}
</style>

View File

@ -77,6 +77,8 @@
mode = newMode mode = newMode
} }
const retrieveMembers = (s: Sprint) => s.members
</script> </script>
<div class="fs-title flex-between header"> <div class="fs-title flex-between header">
@ -171,7 +173,16 @@
size: 'x-small' size: 'x-small'
} }
}, },
{ key: '', presenter: tracker.component.SprintMembersPresenter, props: { kind: 'link' } }, {
key: '',
presenter: contact.component.MembersPresenter,
props: {
kind: 'link',
intlTitle: tracker.string.SprintMembersTitle,
intlSearchPh: tracker.string.SprintMembersSearchPlaceholder,
retrieveMembers
}
},
{ key: '', presenter: SprintDatePresenter, props: { field: 'startDate' } }, { key: '', presenter: SprintDatePresenter, props: { field: 'startDate' } },
{ key: '', presenter: SprintDatePresenter, props: { field: 'targetDate' } }, { key: '', presenter: SprintDatePresenter, props: { field: 'targetDate' } },
{ key: '', presenter: tracker.component.SprintStatusPresenter } { key: '', presenter: tracker.component.SprintStatusPresenter }

View File

@ -1,84 +0,0 @@
<!--
// 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 '@hcengineering/core'
import { Sprint } from '@hcengineering/tracker'
import { Button, showPopup, eventToHTMLElement } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import contact, { Employee } from '@hcengineering/contact'
import { getClient, UsersPopup } from '@hcengineering/presentation'
import { translate } from '@hcengineering/platform'
import tracker from '../../plugin'
export let value: Sprint
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'min-content'
const client = getClient()
let buttonTitle = ''
$: translate(tracker.string.SprintMembersTitle, {}).then((res) => {
buttonTitle = res
})
const handleSprinttMembersChanged = async (result: Ref<Employee>[] | undefined) => {
if (result === undefined) {
return
}
const memberToPull = value.members.filter((x) => !result.includes(x))[0]
const memberToPush = result.filter((x) => !value.members.includes(x))[0]
if (memberToPull) {
await client.update(value, { $pull: { members: memberToPull } })
}
if (memberToPush) {
await client.update(value, { $push: { members: memberToPush } })
}
}
const handleSprintMembersEditorOpened = async (event: MouseEvent) => {
showPopup(
UsersPopup,
{
_class: contact.class.Employee,
selectedUsers: value.members,
allowDeselect: true,
multiSelect: true,
docQuery: {
active: true
},
placeholder: tracker.string.SprintMembersSearchPlaceholder
},
eventToHTMLElement(event),
undefined,
handleSprinttMembersChanged
)
}
</script>
<Button
{kind}
{size}
{width}
{justify}
title={buttonTitle}
icon={tracker.icon.ProjectMembers}
on:click={handleSprintMembersEditorOpened}
/>

View File

@ -15,11 +15,20 @@
<script lang="ts"> <script lang="ts">
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import { Sprint } from '@hcengineering/tracker' import { Sprint } from '@hcengineering/tracker'
import { getCurrentLocation, navigate } from '@hcengineering/ui' import { getCurrentLocation, Icon, navigate, tooltip } from '@hcengineering/ui'
import tracker from '../../plugin'
export let value: WithLookup<Sprint> export let value: WithLookup<Sprint>
export let withIcon = false
export let onClick: () => void | undefined
function navigateToSprint () { function navigateToSprint () {
if (onClick) {
onClick()
}
const loc = getCurrentLocation() const loc = getCurrentLocation()
loc.path[4] = 'sprints'
loc.path[5] = value._id loc.path[5] = value._id
loc.path.length = 6 loc.path.length = 6
navigate(loc) navigate(loc)
@ -28,20 +37,13 @@
{#if value} {#if value}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-presenter flex-grow" on:click={navigateToSprint}> <div class="flex" on:click={navigateToSprint}>
<span title={value.label} class="projectLabel flex-grow">{value.label}</span> {#if withIcon}
<div class="mr-2" use:tooltip={{ label: tracker.string.Sprint }}>
<Icon icon={tracker.icon.Sprint} size={'small'} />
</div>
{/if}
<span title={value.label} class="cursor-pointer fs-bold caption-color overflow-label clear-mins">{value.label}</span
>
</div> </div>
{/if} {/if}
<style lang="scss">
.projectLabel {
display: block;
min-width: 0;
font-weight: 500;
text-align: left;
color: var(--theme-caption-color);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@ -15,12 +15,13 @@
<script lang="ts"> <script lang="ts">
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import type { IssueTemplate } from '@hcengineering/tracker' import type { IssueTemplate } from '@hcengineering/tracker'
import { Icon, showPanel } from '@hcengineering/ui' import { Icon, showPanel, tooltip } from '@hcengineering/ui'
import tracker from '../../plugin' import tracker from '../../plugin'
export let value: WithLookup<IssueTemplate> export let value: WithLookup<IssueTemplate>
// export let inline: boolean = false // export let inline: boolean = false
export let disableClick = false export let disableClick = false
export let noUnderline = false
function handleIssueEditorOpened () { function handleIssueEditorOpened () {
if (disableClick) { if (disableClick) {
@ -34,9 +35,11 @@
{#if value} {#if value}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="issuePresenterRoot flex" class:noPointer={disableClick} on:click={handleIssueEditorOpened}> <span class="issuePresenterRoot" class:noPointer={disableClick} class:noUnderline on:click={handleIssueEditorOpened}>
<Icon icon={tracker.icon.Issues} size={'small'} /> <div class="mr-2" use:tooltip={{ label: tracker.string.IssueTemplate }}>
<span class="ml-2"> <Icon icon={tracker.icon.Issues} size={'small'} />
</div>
<span title={value?.title}>
{title} {title}
</span> </span>
</span> </span>
@ -44,6 +47,8 @@
<style lang="scss"> <style lang="scss">
.issuePresenterRoot { .issuePresenterRoot {
display: flex;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -58,9 +63,18 @@
cursor: default; cursor: default;
} }
&:hover { &.noUnderline {
font-weight: 500;
color: var(--caption-color); color: var(--caption-color);
} }
&:not(.noUnderline) {
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
}
&:active { &:active {
color: var(--accent-color); color: var(--accent-color);
} }

View File

@ -13,10 +13,19 @@
// limitations under the License. // limitations under the License.
// //
import { Class, Client, DocumentQuery, Ref, RelatedDocument, toIdMap, TxOperations } from '@hcengineering/core' import {
Class,
Client,
DocumentQuery,
getCurrentAccount,
Ref,
RelatedDocument,
toIdMap,
TxOperations
} from '@hcengineering/core'
import { Resources, translate } from '@hcengineering/platform' import { Resources, translate } from '@hcengineering/platform'
import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation' import { getClient, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
import { Issue, Sprint, Team } from '@hcengineering/tracker' import { Issue, Scrum, ScrumRecord, Sprint, Team } from '@hcengineering/tracker'
import { showPopup } from '@hcengineering/ui' import { showPopup } from '@hcengineering/ui'
import CreateIssue from './components/CreateIssue.svelte' import CreateIssue from './components/CreateIssue.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte' import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
@ -46,7 +55,6 @@ import EditProject from './components/projects/EditProject.svelte'
import IconPresenter from './components/projects/IconPresenter.svelte' import IconPresenter from './components/projects/IconPresenter.svelte'
import LeadPresenter from './components/projects/LeadPresenter.svelte' import LeadPresenter from './components/projects/LeadPresenter.svelte'
import ProjectEditor from './components/projects/ProjectEditor.svelte' import ProjectEditor from './components/projects/ProjectEditor.svelte'
import ProjectMembersPresenter from './components/projects/ProjectMembersPresenter.svelte'
import ProjectPresenter from './components/projects/ProjectPresenter.svelte' import ProjectPresenter from './components/projects/ProjectPresenter.svelte'
import Projects from './components/projects/Projects.svelte' import Projects from './components/projects/Projects.svelte'
import ProjectStatusEditor from './components/projects/ProjectStatusEditor.svelte' import ProjectStatusEditor from './components/projects/ProjectStatusEditor.svelte'
@ -76,10 +84,12 @@ import SprintEditor from './components/sprints/SprintEditor.svelte'
import SprintPresenter from './components/sprints/SprintPresenter.svelte' import SprintPresenter from './components/sprints/SprintPresenter.svelte'
import Sprints from './components/sprints/Sprints.svelte' import Sprints from './components/sprints/Sprints.svelte'
import SprintSelector from './components/sprints/SprintSelector.svelte' import SprintSelector from './components/sprints/SprintSelector.svelte'
import SprintMembersPresenter from './components/sprints/SprintMembersPresenter.svelte'
import SprintStatusPresenter from './components/sprints/SprintStatusPresenter.svelte' import SprintStatusPresenter from './components/sprints/SprintStatusPresenter.svelte'
import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svelte' import SprintTitlePresenter from './components/sprints/SprintTitlePresenter.svelte'
import Scrums from './components/scrums/Scrums.svelte'
import ScrumRecordPanel from './components/scrums/ScrumRecordPanel.svelte'
import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte' import SubIssuesSelector from './components/issues/edit/SubIssuesSelector.svelte'
import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte' import EstimationEditor from './components/issues/timereport/EstimationEditor.svelte'
import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte' import ReportedTimeEditor from './components/issues/timereport/ReportedTimeEditor.svelte'
@ -104,6 +114,7 @@ import TeamPresenter from './components/teams/TeamPresenter.svelte'
import IssueStatistics from './components/sprints/IssueStatistics.svelte' import IssueStatistics from './components/sprints/IssueStatistics.svelte'
import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte' import StatusRefPresenter from './components/issues/StatusRefPresenter.svelte'
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte' import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
import { EmployeeAccount } from '@hcengineering/contact'
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte' export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
@ -216,6 +227,79 @@ async function deleteSprint (sprint: Sprint): Promise<void> {
} }
} }
async function startRecordingScrum (
client: TxOperations,
newRecordingScrum: Scrum,
previousScrumRecord?: ScrumRecord
): Promise<void> {
const newRecordLabel = `${newRecordingScrum.title}-${newRecordingScrum.scrumRecords ?? 0}`
const startRecord = async (): Promise<void> => {
await client.addCollection(
tracker.class.ScrumRecord,
newRecordingScrum.space,
newRecordingScrum._id,
tracker.class.Scrum,
'scrumRecords',
{
label: newRecordLabel,
scrumRecorder: getCurrentAccount()._id as Ref<EmployeeAccount>,
startTs: Date.now(),
comments: 0
}
)
}
if (previousScrumRecord !== undefined) {
showPopup(
MessageBox,
{
label: tracker.string.ChangeScrumRecord,
message: tracker.string.ChangeScrumRecordConfirm,
params: { previousRecord: previousScrumRecord.label, newRecord: newRecordLabel }
},
undefined,
(result?: boolean) => {
if (result === true) {
void client
.updateCollection(
tracker.class.ScrumRecord,
previousScrumRecord.space,
previousScrumRecord._id,
previousScrumRecord.attachedTo,
tracker.class.Scrum,
'scrumRecords',
{ endTs: Date.now() }
)
.then(async () => await startRecord())
}
}
)
} else {
await startRecord()
}
}
export async function handleRecordingScrum (
client: TxOperations,
currentScrum: Scrum,
activeScrumRecord?: ScrumRecord
): Promise<void> {
// Stop recording scrum if active record attached to current scrum
if (activeScrumRecord?.attachedTo === currentScrum._id) {
await client.updateCollection(
tracker.class.ScrumRecord,
activeScrumRecord.space,
activeScrumRecord._id,
activeScrumRecord.attachedTo,
tracker.class.Scrum,
'scrumRecords',
{ endTs: Date.now() }
)
} else {
await startRecordingScrum(client, currentScrum, activeScrumRecord)
}
}
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
NopeComponent, NopeComponent,
@ -245,7 +329,6 @@ export default async (): Promise<Resources> => ({
IconPresenter, IconPresenter,
LeadPresenter, LeadPresenter,
TargetDatePresenter, TargetDatePresenter,
ProjectMembersPresenter,
ProjectStatusPresenter, ProjectStatusPresenter,
ProjectStatusEditor, ProjectStatusEditor,
SetDueDateActionPopup, SetDueDateActionPopup,
@ -261,7 +344,8 @@ export default async (): Promise<Resources> => ({
CreateIssueTemplate, CreateIssueTemplate,
Sprints, Sprints,
SprintPresenter, SprintPresenter,
SprintMembersPresenter, Scrums,
ScrumRecordPanel,
SprintStatusPresenter, SprintStatusPresenter,
SprintTitlePresenter, SprintTitlePresenter,
SprintSelector, SprintSelector,

View File

@ -244,6 +244,26 @@ export default mergeIds(trackerId, tracker, {
MoveAndDeleteSprint: '' as IntlString, MoveAndDeleteSprint: '' as IntlString,
MoveAndDeleteSprintConfirm: '' as IntlString, MoveAndDeleteSprintConfirm: '' as IntlString,
Scrum: '' as IntlString,
Scrums: '' as IntlString,
ScrumMembersTitle: '' as IntlString,
ScrumMembersSearchPlaceholder: '' as IntlString,
ScrumBeginTime: '' as IntlString,
ScrumEndTime: '' as IntlString,
NewScrum: '' as IntlString,
CreateScrum: '' as IntlString,
ScrumTitlePlaceholder: '' as IntlString,
ScrumDescriptionPlaceholder: '' as IntlString,
ScrumRecords: '' as IntlString,
ScrumRecord: '' as IntlString,
StartRecord: '' as IntlString,
StopRecord: '' as IntlString,
ChangeScrumRecord: '' as IntlString,
ChangeScrumRecordConfirm: '' as IntlString,
ScrumRecorder: '' as IntlString,
ScrumRecordTimeReports: '' as IntlString,
ScrumRecordObjects: '' as IntlString,
Estimation: '' as IntlString, Estimation: '' as IntlString,
ReportedTime: '' as IntlString, ReportedTime: '' as IntlString,
TimeSpendReport: '' as IntlString, TimeSpendReport: '' as IntlString,
@ -311,7 +331,6 @@ export default mergeIds(trackerId, tracker, {
IconPresenter: '' as AnyComponent, IconPresenter: '' as AnyComponent,
LeadPresenter: '' as AnyComponent, LeadPresenter: '' as AnyComponent,
TargetDatePresenter: '' as AnyComponent, TargetDatePresenter: '' as AnyComponent,
ProjectMembersPresenter: '' as AnyComponent,
ProjectStatusPresenter: '' as AnyComponent, ProjectStatusPresenter: '' as AnyComponent,
ProjectStatusEditor: '' as AnyComponent, ProjectStatusEditor: '' as AnyComponent,
SetDueDateActionPopup: '' as AnyComponent, SetDueDateActionPopup: '' as AnyComponent,
@ -328,16 +347,19 @@ export default mergeIds(trackerId, tracker, {
SprintPresenter: '' as AnyComponent, SprintPresenter: '' as AnyComponent,
SprintStatusPresenter: '' as AnyComponent, SprintStatusPresenter: '' as AnyComponent,
SprintTitlePresenter: '' as AnyComponent, SprintTitlePresenter: '' as AnyComponent,
SprintMembersPresenter: '' as AnyComponent,
ReportedTimeEditor: '' as AnyComponent, ReportedTimeEditor: '' as AnyComponent,
TimeSpendReport: '' as AnyComponent, TimeSpendReport: '' as AnyComponent,
EstimationEditor: '' as AnyComponent, EstimationEditor: '' as AnyComponent,
TemplateEstimationEditor: '' as AnyComponent, TemplateEstimationEditor: '' as AnyComponent,
Scrums: '' as AnyComponent,
ScrumRecordPanel: '' as AnyComponent,
ProjectSelector: '' as AnyComponent, ProjectSelector: '' as AnyComponent,
IssueTemplates: '' as AnyComponent, IssueTemplates: '' as AnyComponent,
IssueTemplatePresenter: '' as AnyComponent IssueTemplatePresenter: '' as AnyComponent,
SubIssuesSelector: '' as AnyComponent
}, },
metadata: { metadata: {
CreateIssueDraft: '' as Metadata<IssueDraft> CreateIssueDraft: '' as Metadata<IssueDraft>

View File

@ -258,6 +258,8 @@ export type ProjectsViewMode = 'all' | 'backlog' | 'active' | 'closed'
export type SprintViewMode = 'all' | 'planned' | 'active' | 'closed' export type SprintViewMode = 'all' | 'planned' | 'active' | 'closed'
export type ScrumRecordViewMode = 'timeReports' | 'objects'
export const getIncludedProjectStatuses = (mode: ProjectsViewMode): ProjectStatus[] => { export const getIncludedProjectStatuses = (mode: ProjectsViewMode): ProjectStatus[] => {
switch (mode) { switch (mode) {
case 'all': { case 'all': {
@ -312,6 +314,11 @@ export const sprintTitleMap: Record<SprintViewMode, IntlString> = Object.freeze(
closed: tracker.string.ClosedSprints closed: tracker.string.ClosedSprints
}) })
export const scrumRecordTitleMap: Record<ScrumRecordViewMode, IntlString> = Object.freeze({
timeReports: tracker.string.ScrumRecordTimeReports,
objects: tracker.string.ScrumRecordObjects
})
const listIssueStatusOrder = [ const listIssueStatusOrder = [
tracker.issueStatusCategory.Started, tracker.issueStatusCategory.Started,
tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Unstarted,

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { Employee } from '@hcengineering/contact' import { Employee, EmployeeAccount } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Markup, Ref, RelatedDocument, Space, Timestamp, Type } from '@hcengineering/core' import type { AttachedDoc, Class, Doc, Markup, Ref, RelatedDocument, Space, Timestamp, Type } from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
@ -357,6 +357,37 @@ export interface Project extends Doc {
documents: number documents: number
} }
/**
* @public
*/
export interface ScrumRecord extends AttachedDoc {
label: string
startTs: Timestamp
endTs?: Timestamp
scrumRecorder: Ref<EmployeeAccount>
comments: number
attachments?: number
space: Ref<Team>
attachedTo: Ref<Scrum>
}
/**
* @public
*/
export interface Scrum extends Doc {
title: string
description?: Markup
beginTime: Timestamp
endTime: Timestamp
members: Ref<Employee>[]
space: Ref<Team>
scrumRecords?: number
attachments?: number
}
/** /**
* @public * @public
*/ */
@ -388,6 +419,8 @@ export default plugin(trackerId, {
TypeIssuePriority: '' as Ref<Class<Type<IssuePriority>>>, TypeIssuePriority: '' as Ref<Class<Type<IssuePriority>>>,
TypeProjectStatus: '' as Ref<Class<Type<ProjectStatus>>>, TypeProjectStatus: '' as Ref<Class<Type<ProjectStatus>>>,
Sprint: '' as Ref<Class<Sprint>>, Sprint: '' as Ref<Class<Sprint>>,
Scrum: '' as Ref<Class<Scrum>>,
ScrumRecord: '' as Ref<Class<ScrumRecord>>,
TypeSprintStatus: '' as Ref<Class<Type<SprintStatus>>>, TypeSprintStatus: '' as Ref<Class<Type<SprintStatus>>>,
TimeSpendReport: '' as Ref<Class<TimeSpendReport>>, TimeSpendReport: '' as Ref<Class<TimeSpendReport>>,
TypeReportedTime: '' as Ref<Class<Type<number>>> TypeReportedTime: '' as Ref<Class<Type<number>>>
@ -432,6 +465,9 @@ export default plugin(trackerId, {
DueDate: '' as Asset, DueDate: '' as Asset,
Parent: '' as Asset, Parent: '' as Asset,
Sprint: '' as Asset, Sprint: '' as Asset,
Scrum: '' as Asset,
Start: '' as Asset,
Stop: '' as Asset,
CategoryBacklog: '' as Asset, CategoryBacklog: '' as Asset,
CategoryUnstarted: '' as Asset, CategoryUnstarted: '' as Asset,

View File

@ -22,17 +22,14 @@
// export let label: IntlString // export let label: IntlString
export let onChange: (value: any) => void export let onChange: (value: any) => void
export let kind: 'no-border' | 'link' = 'no-border' export let kind: 'no-border' | 'link' = 'no-border'
$: withTime = type?.withTime ?? true
$: withShift = type?.withShift ?? true
</script> </script>
<DateRangePresenter <DateRangePresenter
{value} {value}
{withTime} mode={type?.mode}
noShift={!type?.withShift}
editable editable
{kind} {kind}
noShift={!withShift}
on:change={(res) => { on:change={(res) => {
if (res.detail !== undefined) onChange(res.detail) if (res.detail !== undefined) onChange(res.detail)
}} }}

View File

@ -19,7 +19,7 @@
import ClassAttributeBar from './ClassAttributeBar.svelte' import ClassAttributeBar from './ClassAttributeBar.svelte'
export let object: Doc export let object: Doc
export let mixins: Mixin<Doc>[] export let mixins: Mixin<Doc>[] = []
export let ignoreKeys: string[] export let ignoreKeys: string[]
export let allowedCollections: string[] = [] export let allowedCollections: string[] = []

View File

@ -32,9 +32,10 @@
export let loadingProps: LoadingProps | undefined = undefined export let loadingProps: LoadingProps | undefined = undefined
export let createItemDialog: AnyComponent | undefined = undefined export let createItemDialog: AnyComponent | undefined = undefined
export let createItemLabel: IntlString | undefined = undefined export let createItemLabel: IntlString | undefined = undefined
export let viewOptionsConfig: ViewOptionModel[] | undefined export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined
export let viewOptions: ViewOptions export let viewOptions: ViewOptions
export let flatHeaders = false export let flatHeaders = false
export let disableHeader = false
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
export let documents: Doc[] | undefined = undefined export let documents: Doc[] | undefined = undefined
@ -154,6 +155,7 @@
on:uncheckAll={uncheckAll} on:uncheckAll={uncheckAll}
on:row-focus on:row-focus
{flatHeaders} {flatHeaders}
{disableHeader}
{props} {props}
/> />
</div> </div>

View File

@ -36,6 +36,7 @@
export let createItemLabel: IntlString | undefined export let createItemLabel: IntlString | undefined
export let viewOptions: ViewOptions export let viewOptions: ViewOptions
export let flatHeaders = false export let flatHeaders = false
export let disableHeader = false
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
export let level: number export let level: number
export let initIndex = 0 export let initIndex = 0
@ -121,6 +122,7 @@
on:uncheckAll on:uncheckAll
on:row-focus on:row-focus
{flatHeaders} {flatHeaders}
{disableHeader}
{props} {props}
/> />
{/key} {/key}

View File

@ -46,6 +46,7 @@
export let itemModels: AttributeModel[] export let itemModels: AttributeModel[]
export let extraHeaders: AnyComponent[] | undefined export let extraHeaders: AnyComponent[] | undefined
export let flatHeaders = false export let flatHeaders = false
export let disableHeader = false
export let props: Record<string, any> = {} export let props: Record<string, any> = {}
export let level: number export let level: number
export let elementByIndex: Map<number, HTMLDivElement> export let elementByIndex: Map<number, HTMLDivElement>
@ -74,7 +75,7 @@
} }
function initCollapsed (singleCat: boolean, lastLevel: boolean, category: any): void { function initCollapsed (singleCat: boolean, lastLevel: boolean, category: any): void {
collapsed = !singleCat && items.length > (lastLevel ? autoFoldLimit : singleCategoryLimit) collapsed = !disableHeader && !singleCat && items.length > (lastLevel ? autoFoldLimit : singleCategoryLimit)
} }
$: initCollapsed(singleCat, lastLevel, category) $: initCollapsed(singleCat, lastLevel, category)
@ -108,27 +109,29 @@
} }
</script> </script>
<ListHeader {#if !disableHeader}
{groupByKey} <ListHeader
{category} {groupByKey}
{space} {category}
{level} {space}
limited={limited.length} {level}
{items} limited={limited.length}
{headerComponent} {items}
{createItemDialog} {headerComponent}
{createItemLabel} {createItemDialog}
{extraHeaders} {createItemLabel}
{newObjectProps} {extraHeaders}
flat={flatHeaders} {newObjectProps}
{props} flat={flatHeaders}
on:more={() => { {props}
limit += 20 on:more={() => {
}} limit += 20
on:collapse={() => { }}
collapsed = !collapsed on:collapse={() => {
}} collapsed = !collapsed
/> }}
/>
{/if}
<ExpandCollapse isExpanded={!collapsed} duration={400}> <ExpandCollapse isExpanded={!collapsed} duration={400}>
{#if !lastLevel} {#if !lastLevel}
<div class="p-2"> <div class="p-2">