diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 055893af58..39587b70e2 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -87,8 +87,7 @@ export default mergeIds(viewId, view, { General: '' as IntlString, Navigation: '' as IntlString, Editor: '' as IntlString, - MarkdownFormatting: '' as IntlString, - List: '' as IntlString + MarkdownFormatting: '' as IntlString }, function: { FilterObjectInResult: '' as Resource<(filter: Filter, onUpdate: () => void) => Promise>>, diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index 8c5f96d631..cf676bb04b 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -122,6 +122,9 @@ --incoming-msg: rgba(67, 67, 72, .3); --outcoming-msg: rgba(67, 67, 72, .6); + --trans-content-05: rgba(138, 143, 152, .05); + --trans-content-10: rgba(138, 143, 152, .1); + --theme-bg-color: #18181e; --theme-bg-selection: #282830; --theme-bg-checked: #262b39; @@ -226,7 +229,7 @@ --dark-color: #90959d; --content-color: #3c4149; --accent-color: #282a30; - --caption-color: #282a30; + --caption-color: #131416; --white-color: #fff; --caret-color: #6e5ed2; --warning-color: #f2994a; // Dark @@ -275,6 +278,9 @@ --incoming-msg: rgba(67, 67, 72, .3); --outcoming-msg: rgba(67, 67, 72, .6); + --trans-content-05: rgba(60, 65, 73, .05); + --trans-content-10: rgba(60, 65, 73, .1); + --theme-bg-color: #FFFFFF; --theme-bg-selection: #F1F1F4; --theme-menu-color: #E7E7E7; diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index 980ff21b30..4affa10917 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -124,7 +124,10 @@ p:last-child { margin-block-end: 0; } .text-center { text-align: center; } -.firstLetter span::first-letter { text-transform: uppercase; } +.firstLetter span{ + display: inline-block; + &::first-letter { text-transform: uppercase; } +} .inline-height2 { line-height: 200%; diff --git a/packages/ui/src/components/Scroller.svelte b/packages/ui/src/components/Scroller.svelte index b82e94afcc..fd0d691aed 100644 --- a/packages/ui/src/components/Scroller.svelte +++ b/packages/ui/src/components/Scroller.svelte @@ -30,6 +30,19 @@ export let noStretch: boolean = false export let divScroll: HTMLElement | undefined = undefined + export function scroll (top: number, left?: number, behavior: 'auto' | 'smooth' = 'auto') { + if (divScroll && divHScroll) { + if (top !== 0) divScroll.scroll({ top, left: 0, behavior }) + if (left !== 0 || left !== undefined) divHScroll.scroll({ top: 0, left, behavior }) + } + } + export function scrollBy (top: number, left?: number, behavior: 'auto' | 'smooth' = 'auto') { + if (divScroll && divHScroll) { + if (top !== 0) divScroll.scrollBy({ top, left: 0, behavior }) + if (left !== 0 || left !== undefined) divHScroll.scrollBy({ top: 0, left, behavior }) + } + } + const dispatch = createEventDispatcher() let mask: 'top' | 'bottom' | 'both' | 'none' = 'none' diff --git a/packages/ui/src/components/TabList.svelte b/packages/ui/src/components/TabList.svelte index 9903f00897..0dde4e98bc 100644 --- a/packages/ui/src/components/TabList.svelte +++ b/packages/ui/src/components/TabList.svelte @@ -43,6 +43,7 @@ {#if items.length > 0}
{#each items as item, i} +
+ + +
{ + time.timelineBox = container.getBoundingClientRect() + time.viewBox = viewbox.getBoundingClientRect() + }} +> +
+
+
+
+
+ {#if time.months} + {#each time.months as month} +
+ {#if month.date.getMonth() === 0} + {month.date.getFullYear()} + {/if} + {month.label} +
+ {/each} + {/if} + {#if time.days} + {#each time.days as day} +
{day.label}
+ {/each} + {/if} +
{time.todayMarker.date.getDate()}
+ +
+
+
+
+
+
+ {#if time.months} + {#each time.months as month} +
+ {/each} + {/if} +
+
+ {#if lines} + + {#each lines as line, row} + {@const rangeRow = time.rows ? time.rows[row] : null} +
x === row) !== undefined} + class:mListGridSelected={selectedRow === row} + on:focus={() => {}} + on:mousemove={(ev) => { + if (row !== selectedRow) { + handleRowFocused(row) + } + ev.preventDefault() + }} + > +
+
+
+ i === row).length > 0} + on:value={(event) => onObjectChecked(row, event.detail)} + /> +
+
+ +
+
+
+ {#if line.items} + {#each line.items as item} + {#if item.startDate} + {@const target = item.targetDate ?? item.startDate + NOT_ENDED} +
+
+ {#if item.icon}{/if} + {#if item.presenter}{/if} + {#if item.label}{item.label}{/if} +
+
+ {/if} + {/each} + {/if} +
+ {#if line.items} + {#if rangeRow !== null && -time.offsetView + time.viewBox.width < rangeRow.min.x} + + {/if} + {#if rangeRow !== null && -time.offsetView > rangeRow.max.x} + + {/if} + {/if} + {#if rangeRow === null && selectedRow === row && time.cursorMarker && !moving} + + {/if} +
+
+ {/each} +
+
+
+
+
+
+ {/if} +
+
+ + diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 60184b85d9..3eb4057c3d 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -100,6 +100,7 @@ export { default as Scroller } from './components/Scroller.svelte' export { default as ScrollerBar } from './components/ScrollerBar.svelte' export { default as TabList } from './components/TabList.svelte' export { default as Chevron } from './components/Chevron.svelte' +export { default as Timeline } from './components/Timeline.svelte' export { default as IconAdd } from './components/icons/Add.svelte' export { default as IconBack } from './components/icons/Back.svelte' diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index f4a1ad3e79..b1b337c5ff 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -13,6 +13,7 @@ // limitations under the License. // import type { Asset, IntlString } from '@hcengineering/platform' +import { Timestamp } from '@hcengineering/core' import { /* Metadata, Plugin, plugin, */ Resource /*, Service */ } from '@hcengineering/platform' import { /* getContext, */ SvelteComponent } from 'svelte' @@ -102,6 +103,7 @@ export interface TabItem { icon?: Asset | AnySvelteComponent color?: string tooltip?: IntlString + action?: () => void } export type ButtonKind = @@ -238,3 +240,43 @@ export interface DeviceOptions { twoRows: boolean theme?: any } + +export interface TimelineItem { + icon?: Asset | AnySvelteComponent + iconSize?: IconSize + iconProps?: Record + presenter?: AnySvelteComponent + props?: Record + label?: string + + startDate: Timestamp + targetDate: Timestamp | undefined +} +export interface TimelineRow { + items: TimelineItem[] | undefined +} +export interface TimelinePoint { + date: Date + x: number + label?: string +} +export interface TimelineMinMax { + min: TimelinePoint + max: TimelinePoint +} +export type TTimelineRow = TimelineMinMax | null +export interface TimelineState { + todayMarker: TimelinePoint + cursorMarker?: TimelinePoint + offsetView: number + renderedRange: { + left: TimelinePoint + right: TimelinePoint + firstDays: number[] + } + rows: TTimelineRow[] | undefined + months: TimelinePoint[] + days: TimelinePoint[] + timelineBox: DOMRect + viewBox: DOMRect +} diff --git a/plugins/tracker-assets/src/index.ts b/plugins/tracker-assets/src/index.ts index 95dd44ff6c..5c48b1d161 100644 --- a/plugins/tracker-assets/src/index.ts +++ b/plugins/tracker-assets/src/index.ts @@ -70,7 +70,9 @@ loadMetadata(tracker.icon, { CopyBranch: `${icons}#copyBranch`, Duplicate: `${icons}#duplicate`, TimeReport: `${icons}#timeReport`, - Estimation: `${icons}#timeReport` + Estimation: `${icons}#timeReport`, + + Timeline: `${icons}#timeline` }) addStringsLoader(trackerId, async (lang: string) => await import(`../lang/${lang}.json`)) diff --git a/plugins/tracker-resources/src/components/projects/ProjectBrowser.svelte b/plugins/tracker-resources/src/components/projects/ProjectBrowser.svelte index b175c950d9..1a717f1262 100644 --- a/plugins/tracker-resources/src/components/projects/ProjectBrowser.svelte +++ b/plugins/tracker-resources/src/components/projects/ProjectBrowser.svelte @@ -18,8 +18,10 @@ import { IntlString } from '@hcengineering/platform' import { createQuery } from '@hcengineering/presentation' import { Project } from '@hcengineering/tracker' - import { Button, IconAdd, Label, showPopup } from '@hcengineering/ui' + import { Button, IconAdd, Label, showPopup, TabList } from '@hcengineering/ui' + import type { TabItem } from '@hcengineering/ui' import tracker from '../../plugin' + import view from '@hcengineering/view' import { getIncludedProjectStatuses, projectsTitleMap, ProjectsViewMode } from '../../utils' import NewProject from './NewProject.svelte' import ProjectsListBrowser from './ProjectsListBrowser.svelte' @@ -28,6 +30,7 @@ export let query: DocumentQuery = {} export let search: string = '' export let mode: ProjectsViewMode = 'all' + export let viewMode: 'list' | 'timeline' = 'list' const ENTRIES_LIMIT = 200 const resultProjectsQuery = createQuery() @@ -72,6 +75,17 @@ mode = newMode } + + const modeList: TabItem[] = [ + { id: 'all', labelIntl: tracker.string.AllProjects, action: () => handleViewModeChanged('all') }, + { id: 'backlog', labelIntl: tracker.string.BacklogProjects, action: () => handleViewModeChanged('backlog') }, + { id: 'active', labelIntl: tracker.string.ActiveProjects, action: () => handleViewModeChanged('active') }, + { id: 'closed', labelIntl: tracker.string.ClosedProjects, action: () => handleViewModeChanged('closed') } + ] + const viewList: TabItem[] = [ + { id: 'list', icon: view.icon.List, tooltip: view.string.List }, + { id: 'timeline', icon: view.icon.Timeline, tooltip: view.string.Timeline } + ]
@@ -84,45 +98,15 @@
-
-
-
-
-
-
-
-
-
-
-
+
+ { + if (result.detail !== undefined && result.detail.action) result.detail.action() + }} + />
- + { + if (result.detail !== undefined && result.detail.id !== viewMode) viewMode = result.detail.id + }} + />
diff --git a/plugins/tracker-resources/src/components/projects/ProjectStatusPresenter.svelte b/plugins/tracker-resources/src/components/projects/ProjectStatusPresenter.svelte index ed6a89b275..87b4f6c55d 100644 --- a/plugins/tracker-resources/src/components/projects/ProjectStatusPresenter.svelte +++ b/plugins/tracker-resources/src/components/projects/ProjectStatusPresenter.svelte @@ -40,28 +40,15 @@ {#if value} - {#if isEditable} - - {:else} - - {/if} + {/if} diff --git a/plugins/tracker-resources/src/components/projects/ProjectsListBrowser.svelte b/plugins/tracker-resources/src/components/projects/ProjectsListBrowser.svelte index d6b1e0809c..5d268dc0c3 100644 --- a/plugins/tracker-resources/src/components/projects/ProjectsListBrowser.svelte +++ b/plugins/tracker-resources/src/components/projects/ProjectsListBrowser.svelte @@ -26,19 +26,23 @@ import { Project } from '@hcengineering/tracker' import { onMount } from 'svelte' import ProjectsList from './ProjectsList.svelte' + import ProjectsTimeline from './ProjectsTimeline.svelte' export let _class: Ref> export let itemsConfig: (BuildModelKey | string)[] export let loadingProps: LoadingProps | undefined = undefined export let projects: Project[] = [] + export let viewMode: 'list' | 'timeline' = 'list' const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => { if (dir === 'vertical') { - projectsList.onElementSelected(offset, of) + if (viewMode === 'list') projectsList.onElementSelected(offset, of) + else projectsTimeline.onElementSelected(offset, of) } }) let projectsList: ProjectsList + let projectsTimeline: ProjectsTimeline $: if (projectsList !== undefined) { listProvider.update(projects) @@ -55,18 +59,36 @@ }} /> - { - listProvider.updateFocus(event.detail ?? undefined) - }} - on:check={(event) => { - listProvider.updateSelection(event.detail.docs, event.detail.value) - }} -/> +{#if viewMode === 'list'} + { + listProvider.updateFocus(event.detail ?? undefined) + }} + on:check={(event) => { + listProvider.updateSelection(event.detail.docs, event.detail.value) + }} + /> +{:else} + { + listProvider.updateFocus(event.detail ?? undefined) + }} + on:check={(event) => { + listProvider.updateSelection(event.detail.docs, event.detail.value) + }} + /> +{/if} diff --git a/plugins/tracker-resources/src/components/projects/ProjectsTimeline.svelte b/plugins/tracker-resources/src/components/projects/ProjectsTimeline.svelte new file mode 100644 index 0000000000..cdd46c495d --- /dev/null +++ b/plugins/tracker-resources/src/components/projects/ProjectsTimeline.svelte @@ -0,0 +1,516 @@ + + + +{#if projects && itemModels && lines} + { + if (ev.detail !== undefined && projects !== undefined) handleRowFocused(projects[ev.detail]) + }} + on:check={(ev) => { + if (ev.detail !== undefined && projects !== undefined) onObjectChecked([projects[ev.detail.row]], ev.detail.value) + }} + > + + {#each itemModels as attributeModel, attributeModelIndex} + {#if attributeModelIndex === 0} +
+
+ +
+
+ {:else if attributeModelIndex === 1} +
+ +
+
+ {:else} +
+ +
+ {/if} + {/each} + + +{:else if loadingProps !== undefined} + {#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex} +
+
+
+ +
+ +
+
+
+
+ {/each} +{/if} + + diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 02dca1246a..6fe7987f86 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -467,7 +467,9 @@ export default plugin(trackerId, { Duplicate: '' as Asset, TimeReport: '' as Asset, - Estimation: '' as Asset + Estimation: '' as Asset, + + Timeline: '' as Asset }, category: { Other: '' as Ref, diff --git a/plugins/view-assets/assets/icons.svg b/plugins/view-assets/assets/icons.svg index 7b791bd42a..1e09562f6e 100644 --- a/plugins/view-assets/assets/icons.svg +++ b/plugins/view-assets/assets/icons.svg @@ -19,6 +19,11 @@ + + + + + diff --git a/plugins/view-assets/lang/en.json b/plugins/view-assets/lang/en.json index 6e318cdab7..1fe816b041 100644 --- a/plugins/view-assets/lang/en.json +++ b/plugins/view-assets/lang/en.json @@ -51,6 +51,7 @@ "View": "View", "MarkupEditor": "Edit of rich content field", "List": "List", + "Timeline": "Timeline", "Select": "Select", "NoGrouping": "No grouping", "Grouping": "Grouping", diff --git a/plugins/view-assets/lang/ru.json b/plugins/view-assets/lang/ru.json index f309ed2339..0dd4bbfbc1 100644 --- a/plugins/view-assets/lang/ru.json +++ b/plugins/view-assets/lang/ru.json @@ -49,6 +49,7 @@ "View": "Вид", "MarkupEditor": "Изменение форматированного поля", "List": "Список", + "Timeline": "Временная шкала", "Select": "Выбрать", "NoGrouping": "Нет группировки", "Grouping": "Группировка", diff --git a/plugins/view-assets/src/index.ts b/plugins/view-assets/src/index.ts index 02c967ebc4..110914c2ef 100644 --- a/plugins/view-assets/src/index.ts +++ b/plugins/view-assets/src/index.ts @@ -21,6 +21,7 @@ loadMetadata(view.icon, { Table: `${icons}#table`, List: `${icons}#list`, Card: `${icons}#card`, + Timeline: `${icons}#timeline`, Delete: `${icons}#delete`, Move: `${icons}#move`, MoreH: `${icons}#more-h`, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 7c746b2b9f..52226ef06e 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -581,12 +581,15 @@ const view = plugin(viewId, { View: '' as IntlString, FilteredViews: '' as IntlString, NewFilteredView: '' as IntlString, - FilteredViewName: '' as IntlString + FilteredViewName: '' as IntlString, + List: '' as IntlString, + Timeline: '' as IntlString }, icon: { Table: '' as Asset, List: '' as Asset, Card: '' as Asset, + Timeline: '' as Asset, Delete: '' as Asset, MoreH: '' as Asset, Move: '' as Asset,