feat(planner): new slots, fixes and improvements (#4961)

Signed-off-by: Eduard Aksamitov <e@euaaaio.ru>
This commit is contained in:
Eduard Aksamitov 2024-03-19 10:40:07 +03:00 committed by GitHub
parent eff7b71f43
commit def45ff217
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 458 additions and 240 deletions

View File

@ -236,7 +236,7 @@ export function createModel (builder: Builder): void {
}
},
label: time.string.CreateToDo,
icon: time.icon.Target,
icon: time.icon.Calendar,
keyBinding: [],
input: 'none',
category: time.category.Time,
@ -262,7 +262,7 @@ export function createModel (builder: Builder): void {
}
},
label: time.string.CreateToDo,
icon: time.icon.Target,
icon: time.icon.Calendar,
keyBinding: [],
input: 'none',
category: time.category.Time,

View File

@ -699,6 +699,7 @@ input.search {
.min-w-8 { min-width: 2rem; }
.min-w-9 { min-width: 2.25rem; }
.min-w-12 { min-width: 3rem; }
.min-w-28 { min-width: 7rem; }
.min-w-50 { min-width: 12.5rem; }
.min-w-60 { min-width: 15rem; }
.min-w-80 { min-width: 20rem; }

View File

@ -26,6 +26,7 @@
--button-negative-active-BackgroundColor: #c42a32;
--tag-on-accent-PorpoiseText: #FFFFFF;
--tag-accent-SunshineBackground: #FFBD2E;
}
/* Dark Theme */
@ -70,6 +71,8 @@
--tag-on-subtle-PorpoiseText: #F2F4F6;
--tag-subtle-PorpoiseBackground: #343F49;
--tag-nuance-SunshineBackground: #262F40;
--tag-accent-SunshineText: #FFBD2E;
--tag-nuance-SkyBackground: #1F2737;
--icon-disabled-IconColor: #394358;
@ -145,6 +148,8 @@
--tag-on-subtle-PorpoiseText: #293139;
--tag-subtle-PorpoiseBackground: #C8D1D9;
--tag-nuance-SunshineBackground: #FEF2E2;
--tag-accent-SunshineText: #8E5E00;
--tag-nuance-SkyBackground: #EEF4FD;
--icon-disabled-IconColor: #B3BCCC;

View File

@ -14,9 +14,11 @@
//
/* Typography */
.font-regular-11,
.font-medium-11,
.font-regular-12,
.font-medium-12,
.font-caps-medium-12,
.font-bold-12,
.font-regular-14,
.font-medium-14,
@ -30,11 +32,13 @@
line-height: 1rem;
color: var(--global-primary-TextColor);
}
.font-regular-11,
.font-medium-11 {
font-size: 0.6875rem;
}
.font-regular-12,
.font-medium-12,
.font-caps-medium-12,
.font-bold-12 {
font-size: 0.75rem;
}
@ -44,6 +48,7 @@
.paragraph-regular-14 {
font-size: 0.875rem;
}
.font-regular-11,
.font-regular-12,
.font-regular-14,
.paragraph-regular-14 {
@ -51,6 +56,7 @@
}
.font-medium-11,
.font-medium-12,
.font-caps-medium-12,
.font-medium-14,
.heading-medium-16,
.heading-medium-20 {
@ -74,6 +80,9 @@
line-height: 1.25rem;
color: var(--global-tertiary-TextColor);
}
.font-caps-medium-12 {
text-transform: uppercase;
}
/* Panels */
* {

View File

@ -120,6 +120,9 @@
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: var(--spacing-2_5);
height: var(--spacing-2_5);
@ -159,10 +162,11 @@
}
&.small {
height: var(--global-small-Size);
gap: var(--spacing-0_25);
border-radius: var(--small-BorderRadius);
&.type-button {
padding: 0 var(--spacing-1_5);
padding: 0 var(--spacing-1);
}
&.type-button-icon {
width: var(--global-small-Size);
@ -363,6 +367,7 @@
}
}
}
& > * {
pointer-events: none;
}

View File

@ -0,0 +1,50 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { AnySvelteComponent } from '../types'
import Icon from './Icon.svelte'
import KeyShift from './icons/KeyShift.svelte'
import KeyOption from './icons/KeyOption.svelte'
import KeyCommand from './icons/KeyCommand.svelte'
export let key: string
const predefinedKeys: Record<string, AnySvelteComponent> = {
shift: KeyShift,
option: KeyOption,
command: KeyCommand
}
$: isPredefinedKey = key in predefinedKeys
</script>
<span class="hotkey flex-center min-w-4 h-4 font-regular-11" class:text={!isPredefinedKey}>
{#if isPredefinedKey}
<Icon icon={predefinedKeys[key]} size="x-small" />
{:else}
{key}
{/if}
</span>
<style lang="scss">
.hotkey {
border-radius: var(--min-BorderRadius);
background-color: var(--global-ui-hover-BackgroundColor);
&.text {
padding: 0 var(--spacing-0_5);
}
}
</style>

View File

@ -0,0 +1,25 @@
<!--
// Copyright © 2024 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 Hotkey from './Hotkey.svelte'
export let keys: string[]
</script>
<div class="flex flex-gap-0-5">
{#each keys as key}
<Hotkey {key} />
{/each}
</div>

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13 11h6V8a5 5 0 1 1 5 5h-3v6h3a5 5 0 1 1-5 5v-3h-6v3a5 5 0 1 1-5-5h3v-6H8a5 5 0 1 1 5-5zm-2-3v3H8a3 3 0 1 1 3-3m2 11h6v-6h-6zm-5 2a3 3 0 1 0 3 3v-3zm13 0v3a3 3 0 1 0 3-3zm0-10V8a3 3 0 1 1 3 3z"
/>
</svg>

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M9.465 8H2V6h8.535l12 18H30v2h-8.535zM18 6h12v2H18z" />
</svg>

View File

@ -0,0 +1,8 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 16v10h8V16h5.593L16 5.037 6.408 16zM2 18h8v10h12V18h8L16 2z" />
</svg>

View File

@ -141,6 +141,8 @@ export { default as NavGroup } from './components/NavGroup.svelte'
export { default as Modal } from './components/Modal.svelte'
export { default as AccordionItem } from './components/AccordionItem.svelte'
export { default as NotificationToast } from './components/NotificationToast.svelte'
export { default as Hotkey } from './components/Hotkey.svelte'
export { default as HotkeyGroup } from './components/HotkeyGroup.svelte'
export { default as IconAdd } from './components/icons/Add.svelte'
export { default as IconCircleAdd } from './components/icons/CircleAdd.svelte'
@ -213,6 +215,9 @@ export { default as IconTableOfContents } from './components/icons/TableOfConten
export { default as IconRight } from './components/icons/Right.svelte'
export { default as IconDropdownDown } from './components/icons/DropdownDown.svelte'
export { default as IconDropdownRight } from './components/icons/DropdownRight.svelte'
export { default as IconKeyCommand } from './components/icons/KeyCommand.svelte'
export { default as IconKeyOption } from './components/icons/KeyOption.svelte'
export { default as IconKeyShift } from './components/icons/KeyShift.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte'

View File

@ -14,9 +14,9 @@
-->
<script lang="ts">
import ui, {
Button,
ButtonKind,
ButtonSize,
ButtonBase,
ButtonBaseKind,
ButtonBaseSize,
DatePopup,
SimpleDatePopup,
TimeInputBox,
@ -33,8 +33,8 @@
export let direction: 'vertical' | 'horizontal' = 'vertical'
export let showDate: boolean = true
export let withoutTime: boolean
export let kind: ButtonKind = 'ghost'
export let size: ButtonSize = 'medium'
export let kind: ButtonBaseKind = 'tertiary'
export let size: ButtonBaseSize = 'small'
export let disabled: boolean = false
export let focusIndex = -1
export let timeZone: string = getUserTimezone()
@ -77,45 +77,49 @@
<div
class="dateEditor-container {direction}"
class:difference={difference > 0}
class:gap-1-5={direction === 'horizontal'}
class:flex-gap-2={direction === 'horizontal'}
>
{#if showDate || withoutTime}
<Button {kind} {size} padding={'0 .5rem'} {focusIndex} on:click={dateClick} {disabled}>
<svelte:fragment slot="content">
<div class="min-w-28">
<ButtonBase type="type-button" {kind} {size} {disabled} {focusIndex} on:click={dateClick}>
<DateLocalePresenter date={currentDate.getTime()} {timeZone} />
</svelte:fragment>
</Button>
</ButtonBase>
</div>
{/if}
{#if showDate && !withoutTime && direction === 'horizontal'}
<div class="divider" />
{/if}
{#if !withoutTime}
<Button
<ButtonBase
type="type-button"
{kind}
{size}
padding={'0 .5rem'}
{disabled}
focusIndex={focusIndex !== -1 ? focusIndex + 1 : focusIndex}
on:click={timeClick}
{disabled}
>
<svelte:fragment slot="content">
<TimeInputBox
bind:currentDate
{timeZone}
noBorder
size={'small'}
on:update={(date) => {
updateTime(date.detail)
}}
/>
{#if difference > 0}
<div class="ml-2 flex-no-shrink content-darker-color overflow-label">
<TimeShiftPresenter value={date - difference} exact />
</div>
{/if}
</svelte:fragment>
</Button>
<TimeInputBox
bind:currentDate
{timeZone}
noBorder
size={'small'}
on:update={(date) => {
updateTime(date.detail)
}}
/>
</ButtonBase>
{/if}
</div>
{#if !withoutTime && difference > 0}
<div class="divider" />
<div class="duration font-regular-14">
<TimeShiftPresenter value={date - difference} exact />
</div>
{/if}
<style lang="scss">
.dateEditor-container {
display: flex;
@ -131,8 +135,19 @@
align-items: start;
}
&:not(.difference) {
align-items: stretch;
align-items: start;
}
}
}
.duration {
padding: var(--spacing-1);
color: var(--tag-accent-SunshineText);
}
.divider {
width: 0;
height: 1.25rem;
border-left: 1px solid var(--global-ui-BorderColor);
}
</style>

View File

@ -241,7 +241,7 @@
.eventPopup-container {
display: flex;
flex-direction: column;
max-width: 25rem;
max-width: 40rem;
min-width: 25rem;
min-height: 0;
background: var(--theme-popup-color);

View File

@ -13,10 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { Icon, IconArrowRight, areDatesEqual, getUserTimezone } from '@hcengineering/ui'
import { utcToZonedTime } from 'date-fns-tz'
import { areDatesEqual, getUserTimezone } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import { utcToZonedTime } from 'date-fns-tz'
import DateEditor from './DateEditor.svelte'
export let startDate: number
@ -50,10 +49,7 @@
}
</script>
<div class="flex-row-center">
<div class="self-start flex-no-shrink mt-2 mr-1-5 content-dark-color">
<Icon icon={calendar.icon.Watch} size={'small'} />
</div>
<div class="flex-row-center flex-gap-2">
<DateEditor
bind:date={startDate}
direction={sameDate ? 'horizontal' : 'vertical'}
@ -63,9 +59,7 @@
{disabled}
{focusIndex}
/>
<div class="self-end flex-no-shrink mb-2 ml-1-5 mr-1-5 content-darker-color">
<IconArrowRight size={'small'} />
</div>
<div class="flex-no-shrink content-darker-color"></div>
<DateEditor
bind:date={dueDate}
direction={sameDate ? 'horizontal' : 'vertical'}

View File

@ -164,7 +164,7 @@
}}
/>
<div class="flex-row-center">
<DateEditor bind:date={until} withoutTime kind="regular" disabled={selected !== 'on'} />
<DateEditor bind:date={until} withoutTime kind="secondary" disabled={selected !== 'on'} />
</div>
<RadioButton
labelIntl={calendar.string.After}

View File

@ -24,11 +24,6 @@
<symbol id="hashtag" viewBox="0 0 32 32">
<path d="M13.9839 5.18321C14.0828 4.63985 13.7224 4.11922 13.1791 4.02035C12.6357 3.92149 12.1151 4.28183 12.0162 4.82519L11.0744 10.0011L6.99205 10.0007C6.43976 10.0006 5.992 10.4483 5.99194 11.0006C5.99189 11.5529 6.43955 12.0006 6.99184 12.0007L10.7105 12.0011L9.25503 20.0005L4.99996 20.0007C4.44767 20.0007 3.99998 20.4484 4 21.0007C4.00002 21.553 4.44776 22.0007 5.00004 22.0007L8.89112 22.0005L8.01389 26.8218C7.91502 27.3651 8.27536 27.8858 8.81872 27.9846C9.36208 28.0835 9.88271 27.7232 9.98158 27.1798L10.924 22.0004L18.8885 22.0001L18.0106 26.8181C17.9116 27.3614 18.2718 27.8821 18.8152 27.9811C19.3585 28.0801 19.8792 27.7199 19.9782 27.1766L20.9214 22L25 21.9998C25.5523 21.9998 26 21.5521 26 20.9998C26 20.4475 25.5522 19.9998 25 19.9998L21.2858 20L22.743 12.0023L27.0001 12.0028C27.5524 12.0028 28.0001 11.5552 28.0002 11.0029C28.0003 10.4506 27.5526 10.0028 27.0003 10.0028L23.1074 10.0024L23.9852 5.1848C24.0842 4.64146 23.724 4.12074 23.1806 4.02174C22.6373 3.92274 22.1166 4.28295 22.0176 4.82629L21.0745 10.0021L13.1072 10.0013L13.9839 5.18321ZM12.7433 12.0013L20.7101 12.0021L19.2529 20.0001L11.2879 20.0004L12.7433 12.0013Z" />
</symbol>
<symbol id="target" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6.5" stroke="currentColor"/>
<circle cx="8" cy="8" r="3.5" stroke="currentColor"/>
<circle cx="8" cy="8" r="0.5" stroke="currentColor"/>
</symbol>
<symbol id="flag" viewBox="0 0 14 14" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.75 1.3125C1.75 1.07088 1.94588 0.875 2.1875 0.875H11.8125C11.9715 0.875 12.1181 0.961309 12.1952 1.10041C12.2723 1.23952 12.2678 1.40951 12.1835 1.54437L10.1409 4.8125L12.1835 8.08063C12.2678 8.21549 12.2723 8.38548 12.1952 8.52459C12.1181 8.66369 11.9715 8.75 11.8125 8.75H2.625V12.6875C2.625 12.9291 2.42912 13.125 2.1875 13.125C1.94588 13.125 1.75 12.9291 1.75 12.6875V1.3125ZM2.625 7.875H11.0231L9.254 5.04437C9.16533 4.90251 9.16533 4.72249 9.254 4.58063L11.0231 1.75H2.625V7.875Z" fill="currentColor"/>
</symbol>
@ -44,4 +39,7 @@
<path d="M9.99998 18.7501C9.89651 18.75 9.79468 18.7243 9.70367 18.6751L2.12886 14.5963C1.82502 14.4327 1.71137 14.0537 1.87505 13.7499C2.03867 13.4462 2.41749 13.3326 2.72124 13.4962L9.99998 17.4152L17.2785 13.4963C17.5823 13.3327 17.9613 13.4464 18.125 13.7502C18.2886 14.0541 18.1749 14.4332 17.871 14.5968L10.2963 18.6755C10.2052 18.7246 10.1034 18.7502 9.99998 18.7501Z" fill="currentColor"/>
<path d="M9.99998 11.2501C9.89651 11.25 9.79468 11.2243 9.70367 11.1751L1.57867 6.80005C1.47934 6.74654 1.39635 6.66713 1.33851 6.57026C1.28066 6.47338 1.25012 6.36266 1.25012 6.24983C1.25012 6.13701 1.28066 6.02628 1.33851 5.92941C1.39635 5.83254 1.47934 5.75313 1.57867 5.69961L9.70367 1.32461C9.7947 1.27548 9.89653 1.24976 9.99998 1.24976C10.1034 1.24976 10.2053 1.27548 10.2963 1.32461L18.4213 5.69961C18.5206 5.75313 18.6036 5.83254 18.6615 5.92941C18.7193 6.02628 18.7498 6.13701 18.7498 6.24983C18.7498 6.36266 18.7193 6.47338 18.6615 6.57026C18.6036 6.66713 18.5206 6.74654 18.4213 6.80005L10.2963 11.1751C10.2053 11.2243 10.1034 11.25 9.99998 11.2501ZM3.19336 6.25005L9.99998 9.91524L16.8066 6.25005L9.99998 2.58493L3.19336 6.25005Z" fill="currentColor"/>
</symbol>
<symbol id="calendar" viewBox="0 0 32 32" fill="none">
<path clip-rule="evenodd" fill-rule="evenodd" fill="currentColor" d="M28 24.005C28 26.214 26.21 28 24 28H8a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4h2V3a1 1 0 1 1 2 0v1h8V3a1 1 0 1 1 2 0v1h2a4 4 0 0 1 4 4zM20 6v1a1 1 0 1 0 2 0V6h2a2 2 0 0 1 2 2v4H6V8a2 2 0 0 1 2-2h2v1a1 1 0 1 0 2 0V6zM6 14v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V14z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -55,6 +55,7 @@
"CreatedToDo": "Created Todo",
"NewToDoDetails": "New Todo: {details}",
"MarkedAsDone": "Completed",
"WorkSchedule": "Work schedule"
"WorkSchedule": "Work schedule",
"SummaryDuration": "Summary"
}
}

View File

@ -55,6 +55,7 @@
"CreatedToDo": "Tarea creada",
"NewToDoDetails": "Nueva tarea pendiente: {details}",
"MarkedAsDone": "Marcado como hecho",
"WorkSchedule": "Horario de trabajo"
"WorkSchedule": "Horario de trabajo",
"SummaryDuration": "Sumerio"
}
}

View File

@ -55,6 +55,7 @@
"CreatedToDo": "Tarefa criada",
"NewToDoDetails": "Nova tarefa pendente: {details}",
"MarkedAsDone": "Concluído",
"WorkSchedule": "Horário de trabalho"
"WorkSchedule": "Horário de trabalho",
"SummaryDuration": "Sumário"
}
}

View File

@ -55,6 +55,7 @@
"CreatedToDo": "Создал(а) Todo",
"NewToDoDetails": "Новое Todo: {details}",
"MarkedAsDone": "Выполнено",
"WorkSchedule": "Расписание работы"
"WorkSchedule": "Расписание работы",
"SummaryDuration": "Всего"
}
}

View File

@ -21,7 +21,7 @@ loadMetadata(time.icon, {
Team: `${icons}#team`,
Hashtag: `${icons}#hashtag`,
Inbox: `${icons}#inbox`,
Target: `${icons}#target`,
Calendar: `${icons}#calendar`,
Flag: `${icons}#flag`,
FilledFlag: `${icons}#filledFlag`,
Planned: `${icons}#planned`,

View File

@ -13,22 +13,22 @@
// limitations under the License.
-->
<script lang="ts">
import { Calendar, generateEventId } from '@hcengineering/calendar'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import { PersonAccount } from '@hcengineering/contact'
import core, { AttachedData, Doc, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
import { Button, Component, EditBox, IconClose, Label, Scroller } from '@hcengineering/ui'
import { SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
import tagsPlugin, { TagReference } from '@hcengineering/tags'
import task from '@hcengineering/task'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Button, Component, EditBox, IconClose, Label } from '@hcengineering/ui'
import { ToDo, ToDoPriority, WorkSlot } from '@hcengineering/time'
import { Calendar, generateEventId } from '@hcengineering/calendar'
import tagsPlugin, { TagReference } from '@hcengineering/tags'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
import DueDateEditor from './DueDateEditor.svelte'
import PriorityEditor from './PriorityEditor.svelte'
import Workslots from './Workslots.svelte'
import { VisibilityEditor } from '@hcengineering/calendar-resources'
import { StyledTextBox } from '@hcengineering/text-editor'
import { PersonAccount } from '@hcengineering/contact'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import task from '@hcengineering/task'
import PriorityEditor from './PriorityEditor.svelte'
import DueDateEditor from './DueDateEditor.svelte'
import Workslots from './Workslots.svelte'
import time from '../plugin'
export let object: Doc | undefined
@ -185,71 +185,72 @@
/>
</div>
</div>
<div class="block flex-no-shrink">
<div class="pb-4">
<StyledTextBox
alwaysEdit={true}
maxHeight="limited"
showButtons={false}
placeholder={calendar.string.Description}
bind:content={todo.description}
/>
</div>
<div class="flex-row-center gap-1-5 mb-1">
<DueDateEditor bind:value={todo.dueDate} />
<PriorityEditor bind:value={todo.priority} />
</div>
</div>
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<div>
<Label label={time.string.AddTo} />
<Scroller>
<div class="block flex-no-shrink">
<div class="pb-4">
<StyledTextBox
alwaysEdit={true}
maxHeight="limited"
showButtons={false}
placeholder={calendar.string.Description}
bind:content={todo.description}
/>
</div>
<div class="flex-row-center gap-1-5 mb-1">
<PriorityEditor bind:value={todo.priority} />
<VisibilityEditor size="small" bind:value={todo.visibility} />
<DueDateEditor bind:value={todo.dueDate} />
</div>
<SpaceSelector
_class={task.class.Project}
query={{ archived: false, members: getCurrentAccount()._id }}
label={core.string.Space}
autoSelect={false}
allowDeselect
kind={'regular'}
size={'medium'}
focus={false}
bind:space={todo.attachedSpace}
/>
<VisibilityEditor bind:value={todo.visibility} />
</div>
</div>
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<Component
is={tagsPlugin.component.DraftTagsEditor}
props={{ tags, targetClass: time.class.ToDo }}
on:change={(e) => {
tags = e.detail
}}
/>
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<div>
<Label label={time.string.AddTo} />
</div>
<SpaceSelector
_class={task.class.Project}
query={{ archived: false, members: getCurrentAccount()._id }}
label={core.string.Space}
autoSelect={false}
allowDeselect
kind={'regular'}
size={'medium'}
focus={false}
bind:space={todo.attachedSpace}
/>
</div>
</div>
</div>
<div class="block flex-no-shrink end">
<div class="flex-row-center gap-1-5">
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<Component
is={tagsPlugin.component.DraftTagsEditor}
props={{ tags, targetClass: time.class.ToDo }}
on:change={(e) => {
tags = e.detail
}}
/>
</div>
</div>
<div class="block flex-gap-4 flex-no-shrink end">
<Workslots
bind:slots
shortcuts={false}
on:remove={removeSlot}
on:create={createSlot}
on:change={changeSlot}
on:dueChange={changeDueSlot}
/>
</div>
</div>
<div class="flex-row-reverse btn flex-no-shrink">
<Button
kind="primary"
{loading}
label={time.string.AddToDo}
on:click={saveToDo}
disabled={todo?.title === undefined || todo?.title === ''}
/>
</div>
<div class="flex-row-reverse btn flex-no-shrink">
<Button
kind="primary"
{loading}
label={time.string.AddToDo}
on:click={saveToDo}
disabled={todo?.title === undefined || todo?.title === ''}
/>
</div>
</Scroller>
</div>
<style lang="scss">

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import ui, { Button, DatePopup, Icon, Label, showPopup } from '@hcengineering/ui'
import ui, { ButtonBase, DatePopup, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
@ -22,16 +22,20 @@
const dispatch = createEventDispatcher()
let opened: boolean = false
</script>
<Button
kind={'regular'}
on:click={(e) => {
$: buttonTitle = value ? new Date(value).toLocaleDateString() : undefined
$: buttonLabel = buttonTitle === undefined ? ui.string.DueDate : undefined
function handleClick (e: MouseEvent) {
if (!opened) {
opened = true
showPopup(
DatePopup,
{ noShift: true, currentDate: value ? new Date(value) : null, label: ui.string.SetDueDate },
{
noShift: true,
currentDate: value ? new Date(value) : null,
label: ui.string.SetDueDate
},
'top',
(result) => {
if (result != null && result.value !== undefined) {
@ -42,14 +46,17 @@
}
)
}
}}
>
<div slot="content" class="flex-row-center flex-gap-1">
<Icon icon={time.icon.Target} size="medium" />
{#if value}
{new Date(value).toLocaleDateString()}
{:else}
<Label label={ui.string.DueDate} />
{/if}
</div>
</Button>
}
</script>
<ButtonBase
kind="secondary"
size="small"
type="type-button"
icon={time.icon.Calendar}
iconSize="small"
title={buttonTitle}
label={buttonLabel}
pressed={opened}
on:click={handleClick}
/>

View File

@ -190,8 +190,8 @@
</div>
{/if}
<div class="slots-content">
<div class="flex-row-top justify-between flex-gap-2">
<span class="font-medium-14 secondary-textColor">
<div class="flex-row-top justify-between items-center flex-gap-2">
<span class="font-caps-medium-12 slots-content-title">
<Label label={time.string.WorkSchedule} />
</span>
<div class="flex-row-center gap-2">
@ -235,9 +235,8 @@
}
.slots-content {
gap: var(--spacing-2);
padding: var(--spacing-3) var(--spacing-4);
padding: var(--spacing-2) var(--spacing-4);
border-top: 1px solid var(--theme-divider-color);
border-bottom: 1px solid var(--theme-divider-color);
}
.eventPopup-container {
display: flex;

View File

@ -46,7 +46,7 @@
}
})
$: selected = selectPopupPriorities.find((item) => item.id === value)
$: selectedLabel = selected?.label ?? time.string.NoPriority
$: selectedLabel = selected?.label === time.string.NoPriority ? time.string.SetPriority : selected?.label
$: icon = selected?.id === ToDoPriority.NoPriority ? time.icon.Flag : selected?.icon
$: iconProps = selected?.iconProps

View File

@ -1,35 +1,14 @@
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { DAY, HOUR, MINUTE, themeStore } from '@hcengineering/ui'
import { themeStore } from '@hcengineering/ui'
import { WorkSlot } from '@hcengineering/time'
import time from '../plugin'
import { calculateEventsDuration, formatEventsDuration } from '../utils'
export let events: WorkSlot[]
$: duration = events.reduce((acc, curr) => acc + curr.dueDate - curr.date, 0)
let res: string = ''
async function formatTime (value: number) {
res = ''
const days = Math.floor(value / DAY)
if (days > 0) {
res += await translate(time.string.Days, { days }, $themeStore.language)
}
const hours = Math.floor((value % DAY) / HOUR)
if (hours > 0) {
res += ' '
res += await translate(time.string.Hours, { hours }, $themeStore.language)
}
const minutes = Math.floor((value % HOUR) / MINUTE)
if (minutes > 0) {
res += ' '
res += await translate(time.string.Minutes, { minutes }, $themeStore.language)
}
res = res.trim()
}
$: formatTime(duration)
let duration: string
$: formatEventsDuration(calculateEventsDuration(events), $themeStore.language).then((res) => {
duration = res
})
</script>
{res}
{duration}

View File

@ -13,15 +13,31 @@
// limitations under the License.
-->
<script lang="ts">
import { ButtonBase, ButtonIcon, IconDelete, themeStore, Hotkey, HotkeyGroup } from '@hcengineering/ui'
import { EventTimeEditor } from '@hcengineering/calendar-resources'
import { ActionIcon, Button, Icon, IconCircleAdd, IconClose, Scroller } from '@hcengineering/ui'
import { WorkSlot } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
import { calculateEventsDuration, formatEventsDuration } from '../utils'
import Label from '@hcengineering/ui/src/components/Label.svelte'
export let slots: WorkSlot[] = []
export let shortcuts: boolean = true
const dispatch = createEventDispatcher()
let duration: string
$: formatEventsDuration(calculateEventsDuration(slots), $themeStore.language).then((res) => {
duration = res
})
function handleKeyDown (event: KeyboardEvent): void {
if (!shortcuts) return
if (event.shiftKey && event.key === 'Enter') {
dispatch('create')
}
}
async function change (e: CustomEvent<{ startDate: number, dueDate: number }>, slot: WorkSlot): Promise<void> {
const { startDate, dueDate } = e.detail
dispatch('change', { startDate, dueDate, slot: slot._id })
@ -33,50 +49,77 @@
}
</script>
<div class="flex-col container w-full flex-gap-1">
<Scroller>
{#each slots as slot}
<div class="flex-between w-full pr-4 slot">
<EventTimeEditor
allDay={false}
startDate={slot.date}
bind:dueDate={slot.dueDate}
on:change={(e) => change(e, slot)}
on:dueChange={(e) => dueChange(e, slot)}
<svelte:window on:keydown={handleKeyDown} />
<div class="flex-col w-full flex-gap-1">
{#each slots as slot, i}
<div class="flex justify-start items-center flex-gap-2 w-full pr-4 slot">
<Hotkey key={(i + 1).toString()} />
<EventTimeEditor
allDay={false}
startDate={slot.date}
dueDate={slot.dueDate}
on:change={(e) => change(e, slot)}
on:dueChange={(e) => dueChange(e, slot)}
/>
<div class="tool">
<ButtonIcon
kind="tertiary"
size="small"
icon={IconDelete}
on:click={() => {
dispatch('remove', { _id: slot._id })
}}
/>
<div class="tool">
<ActionIcon
icon={IconClose}
size={'small'}
action={() => {
dispatch('remove', { _id: slot._id })
}}
/>
</div>
</div>
{/each}
</Scroller>
<div class="flex-row-center">
<div class="mr-1-5">
<Icon icon={IconCircleAdd} size="small" />
</div>
<Button padding={'0 .5rem'} kind="ghost" label={time.string.AddSlot} on:click={() => dispatch('create')} />
</div>
{/each}
</div>
<div class="flex-row-center flex-gap-4">
<ButtonBase
kind="secondary"
type="type-button"
size="medium"
label={time.string.AddSlot}
on:click={() => dispatch('create')}
>
{#if shortcuts}
<HotkeyGroup keys={['shift', 'Enter']} />
{/if}
</ButtonBase>
{#if duration}
<div class="font-regular-14">
<Label label={time.string.SummaryDuration} />:
<br />
<span class="duration">{duration}</span>
</div>
{/if}
</div>
<style lang="scss">
.container {
max-height: 10rem;
.slot {
position: relative;
padding: var(--spacing-1) var(--spacing-1) var(--spacing-1) var(--spacing-2_5);
border-radius: var(--small-BorderRadius);
background-color: var(--tag-nuance-SunshineBackground);
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0.25rem;
height: 100%;
background-color: var(--tag-accent-SunshineBackground);
border-radius: var(--small-BorderRadius) 0 0 var(--small-BorderRadius);
}
.tool {
margin-left: auto;
}
}
.slot {
.tool {
visibility: hidden;
}
&:hover {
.tool {
visibility: visible;
}
}
.duration {
color: var(--tag-accent-SunshineText);
}
</style>

View File

@ -61,6 +61,7 @@ export default mergeIds(timeId, time, {
AddTo: '' as IntlString,
AddTitle: '' as IntlString,
MyWork: '' as IntlString,
WorkSchedule: '' as IntlString
WorkSchedule: '' as IntlString,
SummaryDuration: '' as IntlString
}
})

View File

@ -1,6 +1,9 @@
import type { Client, Ref } from '@hcengineering/core'
import type { DefSeparators } from '@hcengineering/ui'
import type { WorkSlot, ToDo } from '@hcengineering/time'
import type { DefSeparators } from '@hcengineering/ui'
import type { Client, Ref } from '@hcengineering/core'
import { DAY, HOUR, MINUTE } from '@hcengineering/ui'
import { translate } from '@hcengineering/platform'
import timePlugin from './plugin'
import time from '@hcengineering/time'
export * from './types'
@ -36,3 +39,46 @@ export async function ToDoTitleProvider (client: Client, ref: Ref<ToDo>, doc?: T
return object.title
}
export function calculateEventsDuration (events: WorkSlot[]): number {
const points = events.flatMap((event) => [
{ time: event.date, type: 'start' },
{ time: event.dueDate, type: 'end' }
])
points.sort((a, b) => a.time - b.time)
let activeEvents = 0
let duration = 0
let lastTime = 0
points.forEach((point) => {
if (activeEvents > 0) {
duration += point.time - lastTime
}
activeEvents += point.type === 'start' ? 1 : -1
lastTime = point.time
})
return duration
}
export async function formatEventsDuration (duration: number, language: string): Promise<string> {
let text = ''
const days = Math.floor(duration / DAY)
if (days > 0) {
text += await translate(timePlugin.string.Days, { days }, language)
}
const hours = Math.floor((duration % DAY) / HOUR)
if (hours > 0) {
text += ' '
text += await translate(timePlugin.string.Hours, { hours }, language)
}
const minutes = Math.floor((duration % HOUR) / MINUTE)
if (minutes > 0) {
text += ' '
text += await translate(timePlugin.string.Minutes, { minutes }, language)
}
text = text.trim()
return text
}

View File

@ -13,12 +13,12 @@
// limitations under the License.
//
import { Event, Visibility } from '@hcengineering/calendar'
import { Person } from '@hcengineering/contact'
import { AttachedDoc, Class, Doc, Hierarchy, Markup, Mixin, Ref, Space, Timestamp, Type } from '@hcengineering/core'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { AttachedDoc, Class, Doc, Hierarchy, Markup, Mixin, Ref, Space, Timestamp, Type } from '@hcengineering/core'
import { IntlString, plugin } from '@hcengineering/platform'
import { Event, Visibility } from '@hcengineering/calendar'
import { AnyComponent } from '@hcengineering/ui'
import { Person } from '@hcengineering/contact'
/**
* @public
@ -120,7 +120,7 @@ export default plugin(timeId, {
Team: '' as Asset,
Hashtag: '' as Asset,
Inbox: '' as Asset,
Target: '' as Asset,
Calendar: '' as Asset,
Flag: '' as Asset,
FilledFlag: '' as Asset,
Planned: '' as Asset,

View File

@ -1,5 +1,5 @@
# Create workspace record in accounts
./tool-local.sh create-workspace sanity-ws -o SanityTest
./tool-local.sh create-workspace sanity-ws -w SanityTest
# Create user record in accounts
./tool-local.sh create-account user1 -f John -l Appleseed -p 1234
./tool-local.sh confirm-email user1

View File

@ -40,8 +40,12 @@ export class PlanningPage extends CalendarPage {
this.inputPopupCreateTitle = page.locator('div.popup input')
this.inputPopupCreateDescription = page.locator('div.popup div.tiptap')
this.inputPanelCreateDescription = page.locator('div.hulyModal-container div.tiptap')
this.buttonPopupCreateDueDate = page.locator('div.popup button.antiButton', { hasText: 'Due date' })
this.buttonPanelCreateDueDate = page.locator('div.hulyModal-container button.antiButton', { hasText: 'Due date' })
this.buttonPopupCreateDueDate = page.locator(
'div.popup div.block:first-child div.flex-row-center button:nth-child(3)'
)
this.buttonPanelCreateDueDate = page.locator(
'div.hulyModal-container div.slots-content div.flex-row-top.justify-between div.flex-row-center button:first-child'
)
this.buttonPopupCreatePriority = page.locator('div.popup button#priorityButton')
this.buttonPanelCreatePriority = page.locator('div.hulyModal-container button#priorityButton')
this.buttonPopupCreateVisible = page.locator('div.popup button.type-button.menu', { hasText: 'visible' })
@ -50,8 +54,8 @@ export class PlanningPage extends CalendarPage {
})
this.buttonPopupCreateAddLabel = page.locator('div.popup button.antiButton', { hasText: 'Add label' })
this.buttonPanelCreateAddLabel = page.locator('.hulyHeader-titleGroup > button:nth-child(2)')
this.buttonPopupCreateAddSlot = page.locator('div.popup button.antiButton', { hasText: 'Add Slot' })
this.buttonPanelCreateAddSlot = page.locator('div.hulyModal-container button.antiButton', { hasText: 'Add Slot' })
this.buttonPopupCreateAddSlot = page.locator('div.popup button', { hasText: 'Add Slot' })
this.buttonPanelCreateAddSlot = page.locator('div.hulyModal-container button', { hasText: 'Add Slot' })
this.buttonCalendarToday = page.locator('div.popup div.calendar button.day.today')
this.buttonCreateToDo = page.locator('div.popup button.antiButton', { hasText: 'Add ToDo' })
this.inputCreateToDoTitle = page.locator('div.toDos-container input[placeholder="Add todo, press Enter to save"]')
@ -63,7 +67,7 @@ export class PlanningPage extends CalendarPage {
)
this.textPanelToDoDescription = page.locator('div.hulyModal-container div.top-content div.tiptap > p')
this.textPanelDueDate = page.locator(
'div.hulyModal-container div.slots-content div.flex-row-top.justify-between div.flex-row-center button.antiButton:first-child div[slot="content"]'
'div.hulyModal-container div.slots-content div.flex-row-top.justify-between div.flex-row-center button:first-child span'
)
this.textPanelPriority = page.locator('div.hulyModal-container button#priorityButton svg')
this.textPanelVisible = page.locator(
@ -133,12 +137,12 @@ export class PlanningPage extends CalendarPage {
public async setTimeSlot (rowNumber: number, slot: Slot, popup: boolean = false): Promise<void> {
const p = popup
? 'div.popup div.horizontalBox div.flex-row-center'
: 'div.hulyModal-container div.slots-content div.horizontalBox div.flex-row-center'
? 'div.popup div.horizontalBox div.end div.flex-col div.flex'
: 'div.hulyModal-container div.slots-content div.flex-col div.flex'
const row = this.page.locator(p).nth(rowNumber)
// dateStart
await row.locator('div.dateEditor-container:nth-child(2) button:first-child').click()
await row.locator('div.dateEditor-container:nth-child(1) button:first-child').click()
if (slot.dateStart === 'today') {
await this.buttonCalendarToday.click()
} else {
@ -154,34 +158,34 @@ export class PlanningPage extends CalendarPage {
const hours = slot.timeStart.substring(0, 2)
const minutes = slot.timeStart.substring(2, slot.timeStart.length)
await row
.locator('div.dateEditor-container:nth-child(2) button:last-child span.digit:first-child')
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:first-child')
.click({ delay: 200 })
await row
.locator('div.dateEditor-container:nth-child(2) button:last-child span.digit:first-child')
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:first-child')
.pressSequentially(hours, { delay: 100 })
await row
.locator('div.dateEditor-container:nth-child(2) button:last-child span.digit:last-child')
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:last-child')
.click({ delay: 200 })
await row
.locator('div.dateEditor-container:nth-child(2) button:last-child span.digit:last-child')
.locator('div.dateEditor-container:nth-child(1) button:last-child span.digit:last-child')
.pressSequentially(minutes, { delay: 100 })
// dateEnd + timeEnd
await row.locator('div.dateEditor-container:nth-child(4) button').click()
await row.locator('div.dateEditor-container.difference button').click()
await this.fillSelectDatePopup(slot.dateEnd.day, slot.dateEnd.month, slot.dateEnd.year, slot.timeEnd)
}
private async checkTimeSlot (rowNumber: number, slot: Slot, popup: boolean = false): Promise<void> {
const p = popup
? 'div.popup div.horizontalBox div.flex-row-center'
: 'div.hulyModal-container div.slots-content div.horizontalBox div.flex-row-center'
? 'div.popup div.horizontalBox div.end div.flex-col div.flex'
: 'div.hulyModal-container div.slots-content div.flex-col div.flex'
const row = this.page.locator(p).nth(rowNumber)
// timeStart
await expect(row.locator('div.dateEditor-container:nth-child(2) button:last-child div.datetime-input')).toHaveText(
await expect(row.locator('div.dateEditor-container:nth-child(1) button:last-child div.datetime-input')).toHaveText(
slot.timeStart
)
// timeEnd
await expect(row.locator('div.dateEditor-container:nth-child(4) button > div:first-child')).toHaveText(slot.timeEnd)
await expect(row.locator('div.dateEditor-container.difference button > div:first-child')).toHaveText(slot.timeEnd)
}
async openToDoByName (toDoName: string): Promise<void> {
@ -255,18 +259,17 @@ export class PlanningPage extends CalendarPage {
}
public async deleteTimeSlot (rowNumber: number): Promise<void> {
const row = this.page.locator('div.hulyModal-container div.slots-content div.horizontalBox div.tool').nth(rowNumber)
const row = this.page
.locator('div.hulyModal-container div.slots-content div.flex-col div.flex div.tool')
.nth(rowNumber)
await row.locator('xpath=..').hover()
await row.locator('button').click()
await expect(row.locator('button')).toBeHidden()
await this.pressYesDeletePopup(this.page)
}
public async checkTimeSlotEndDate (rowNumber: number, dateEnd: string): Promise<void> {
const row = this.page
.locator('div.hulyModal-container div.slots-content div.horizontalBox div.flex-row-center')
.nth(rowNumber)
const row = this.page.locator('div.hulyModal-container div.slots-content div.flex-col div.flex').nth(rowNumber)
// dateEnd
await expect(row.locator('div.dateEditor-container:nth-child(2) button:first-child')).toContainText(dateEnd)
await expect(row.locator('div.dateEditor-container:nth-child(1) button:first-child')).toContainText(dateEnd)
}
}