Dashboards (#2208)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-07-05 11:38:19 +06:00 committed by GitHub
parent e052aa5e41
commit 7807588b88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 495 additions and 15 deletions

View File

@ -155,6 +155,13 @@ export function createModel (builder: Builder): void {
lead.app.Lead
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: lead.class.Lead,
descriptor: task.viewlet.Dashboard,
options: {},
config: []
})
createAction(builder, { ...actionTemplates.archiveSpace, target: lead.class.Funnel })
createAction(builder, { ...actionTemplates.unarchiveSpace, target: lead.class.Funnel })

View File

@ -364,6 +364,13 @@ export function createModel (builder: Builder): void {
config: []
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Applicant,
descriptor: task.viewlet.Dashboard,
options: {},
config: []
})
builder.mixin(recruit.class.Applicant, core.class.Class, task.mixin.KanbanCard, {
card: recruit.component.KanbanCard
})

View File

@ -467,6 +467,17 @@ export function createModel (builder: Builder): void {
task.viewlet.Kanban
)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: task.string.Dashboard,
icon: task.icon.Dashboard,
component: task.component.Dashboard
},
task.viewlet.Dashboard
)
builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.CollectionEditor, {
editor: task.component.Todos
})

View File

@ -56,7 +56,8 @@ export default mergeIds(taskId, task, {
Todos: '' as AnyComponent,
TodoItemPresenter: '' as AnyComponent,
StatusTableView: '' as AnyComponent,
TaskHeader: '' as AnyComponent
TaskHeader: '' as AnyComponent,
Dashboard: '' as AnyComponent
},
space: {
TasksPublic: '' as Ref<Space>

View File

@ -502,6 +502,7 @@ input.search {
.max-h-125 { max-height: 31.25rem; }
.max-h-60 { max-height: 15rem; }
.max-w-60 { max-width: 15rem; }
.max-w-240 { max-width: 60rem; }
.clear-mins {
min-width: 0;
min-height: 0;

View File

@ -0,0 +1,47 @@
<!--
// 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 { DashboardItem } from '../types'
import MultiProgress from './MultiProgress.svelte'
export let items: DashboardItem[] = []
$: max = Math.max(...items.map((p) => p.values.reduce((acc, val) => (acc += val.value), 0)))
</script>
<div class="grid">
{#each items as item (item.label)}
<div>
{item.label}
</div>
<div class="w-full max-w-240">
<MultiProgress {max} values={item.values} />
</div>
{/each}
</div>
<style lang="scss">
.grid {
display: grid;
grid-auto-flow: column;
justify-content: flex-start;
align-items: center;
row-gap: 1rem;
column-gap: 1rem;
grid-template-columns: 1fr 5fr;
grid-auto-flow: row;
}
</style>

View File

@ -0,0 +1,94 @@
<!--
// 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 { getPlatformColor } from '../colors'
export let values: Progress[]
export let min: number = 0
export let max: number = 100
interface Progress {
value: number
color: number
}
$: filtred = values.filter((p) => p.value > min)
$: proc = (max - min) / 100
const width: number[] = []
$: width.length = filtred.length
function getLeft (width: number[], i: number): number {
let res = 0
for (let index = 0; index < width.length; index++) {
if (index === i) break
res += width[index]
}
return res
}
function getWidth (values: Progress[], i: number): number {
let value = values[i].value
if (value > max) value = max
if (value < min) value = min
const res = Math.round((value - min) / proc)
width[i] = res
return res
}
</script>
<div class="container">
{#each filtred as item, i}
<div
class="bar fs-title"
class:first={i === 0}
class:last={i === filtred.length - 1}
style="background-color: {getPlatformColor(item.color)}; left: {getLeft(width, i)}%; width: calc(100% * {proc !==
0
? getWidth(filtred, i)
: 0} / 100);"
>
{item.value}
</div>
{/each}
</div>
<style lang="scss">
.container {
position: relative;
width: 100%;
height: 1.5rem;
background-color: var(--theme-bg-accent-hover);
border-radius: 0.25rem;
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
padding-left: 0.5rem;
padding-top: 0.125rem;
&.first {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
&.last {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
}
}
</style>

View File

@ -1,14 +1,14 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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.
-->
@ -25,12 +25,11 @@
if (value < min) value = min
function click (e: MouseEvent) {
if (!editable) return
const rect = (e.target as HTMLElement).getBoundingClientRect()
const x = e.clientX - rect.left
const pos = x / rect.width
value = (max - min) * pos
console.log(`set value to ${value}`)
console.log(`max value ${max}`)
}
</script>

View File

@ -143,6 +143,7 @@ export { default as FocusHandler } from './components/FocusHandler.svelte'
export { default as ListView } from './components/ListView.svelte'
export { default as ToggleButton } from './components/ToggleButton.svelte'
export { default as ExpandCollapse } from './components/ExpandCollapse.svelte'
export { default as BarDashboard } from './components/BarDashboard.svelte'
export * from './types'
export * from './location'

View File

@ -188,3 +188,14 @@ export interface PopupOptions {
direction: string
fullSize?: boolean
}
export interface DashboardItem {
label: string
values: DashboardGroup[]
tooltip?: LabelAndProps
}
export interface DashboardGroup {
value: number
color: number
}

View File

@ -28,4 +28,10 @@
<path d="M13.3,8.3c-0.1,2.8-2.5,5.1-5.4,5.1C5,13.4,2.6,11,2.6,8c0-2.9,2.3-5.2,5.1-5.4c0.1-0.4,0.2-0.7,0.4-1c0,0-0.1,0-0.1,0 C4.4,1.7,1.6,4.5,1.6,8c0,3.5,2.9,6.4,6.4,6.4s6.4-2.9,6.4-6.4c0,0,0-0.1,0-0.1C14,8.1,13.7,8.2,13.3,8.3z"/>
<ellipse cx="12.1" cy="3.9" rx="2.5" ry="2.5"/>
</symbol>
<symbol id="dashboard" viewBox="0 0 256 256">
<g style="stroke:none;stroke-width:0;stroke-dasharray:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;">
<path d="M27.429 72.429a4 4 0 0 1-4-4v-11.8a4 4 0 0 1 8 0v11.8a4 4 0 0 1-4 4zm17.571 0a4 4 0 0 1-4-4V45a4 4 0 0 1 8 0v23.429a4 4 0 0 1-4 4zm17.571 0a4 4 0 0 1-4-4V21.571a4 4 0 0 1 8 0v46.857a4 4 0 0 1-4 4.001z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;" transform="matrix(2.8 0 0 2.8 1.964 1.964)"/>
<path d="M77.403 90H12.597C5.651 90 0 84.35 0 77.403V12.597C0 5.651 5.651 0 12.597 0h64.807C84.35 0 90 5.651 90 12.597v64.807C90 84.35 84.35 90 77.403 90zM12.597 8A4.602 4.602 0 0 0 8 12.597v64.807A4.602 4.602 0 0 0 12.597 82h64.807A4.602 4.602 0 0 0 82 77.403V12.597A4.602 4.602 0 0 0 77.403 8H12.597z" style="stroke:none;stroke-width:1;stroke-dasharray:none;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;" transform="matrix(2.8 0 0 2.8 1.964 1.964)"/>
</g>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -62,7 +62,7 @@
"DoneStatesLost": "Done status / Lost",
"AllStates": "All states",
"DoneStates": "Done states",
"States": "States",
"States": "States",
"NoDoneState": "Not done",
"ManageStatusesWithin": "Manage application statuses within",
"ManageProjectStatues": "Manage project statues",
@ -74,6 +74,8 @@
"CantStatusDeleteError": "There are objects in the given state. Move or delete them first.",
"Tasks": "Tasks",
"Assigned": "Assigned to me",
"TodoItems": "Todos"
"TodoItems": "Todos",
"Dashboard": "Dashboard",
"AllTime": "All time"
}
}

View File

@ -74,6 +74,8 @@
"CantStatusDeleteError": "Есть объекты с данным статусом. Сначала переместите или удалите их. ",
"Tasks": "Задачи",
"Assigned": "Назначения",
"TodoItems": "Todos"
"TodoItems": "Todos",
"Dashboard": "Дашборд",
"AllTime": "Все время"
}
}

View File

@ -23,7 +23,8 @@ loadMetadata(task.icon, {
TodoCheck: `${icons}#todo-check`,
TodoUnCheck: `${icons}#todo-uncheck`,
ManageStatuses: `${icons}#manage-statuses`,
TaskState: `${icons}#task-state`
TaskState: `${icons}#task-state`,
Dashboard: `${icons}#dashboard`
})
addStringsLoader(taskId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,62 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Timestamp } from '@anticrm/core'
import task from '../plugin'
import { eventToHTMLElement, Label, showPopup } from '@anticrm/ui'
import { TimestampPresenter } from '@anticrm/view-resources'
import CreateFilterPopup from './CreateFilterPopup.svelte'
export let value: Timestamp | undefined
</script>
<button
class="mb-4 ml-10"
on:click={(e) => {
showPopup(CreateFilterPopup, {}, eventToHTMLElement(e), (e) => {
value = e
})
}}
>
{#if value}
<TimestampPresenter {value} />
{:else}
<Label label={task.string.AllTime} />
{/if}
</button>
<style lang="scss">
button {
display: flex;
align-items: center;
width: fit-content;
margin-right: 1px;
padding: 0 0.375rem;
font-size: 0.75rem;
height: 1.5rem;
white-space: nowrap;
color: var(--accent-color);
background-color: var(--noborder-bg-color);
border: 1px solid transparent;
transition-property: border, background-color, color, box-shadow;
transition-duration: 0.15s;
border-radius: 0.25rem;
&:hover {
color: var(--caption-color);
background-color: var(--noborder-bg-hover);
}
}
</style>

View File

@ -0,0 +1,68 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Timestamp } from '@anticrm/core'
import { closeTooltip, Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
import { TimestampPresenter } from '@anticrm/view-resources'
const dispatch = createEventDispatcher()
function click (value: Timestamp | undefined): void {
closeTooltip()
dispatch('close', value)
}
const today = new Date().setHours(0, 0, 0, 0)
function shiftDays (diff: number): number {
return new Date(today).setDate(new Date(today).getDate() - diff)
}
const values = [
shiftDays(1),
shiftDays(7),
shiftDays(14),
shiftDays(30),
shiftDays(90),
shiftDays(180),
shiftDays(365)
]
</script>
<div class="selectPopup">
<div class="scroll">
<div class="box">
<div
class="menu-item"
on:click={() => {
click(undefined)
}}
>
<Label label={task.string.AllTime} />
</div>
{#each values as value, i}
<div
class="menu-item"
on:click={() => {
click(value)
}}
>
<TimestampPresenter {value} />
</div>
{/each}
</div>
</div>
</div>

View File

@ -0,0 +1,153 @@
<!--
// 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 core, { Class, DocumentQuery, Ref, SortingOrder, Timestamp } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import type { DoneState, SpaceWithStates, State, Task } from '@anticrm/task'
import task from '@anticrm/task'
import { BarDashboard, DashboardItem } from '@anticrm/ui'
import { FilterBar } from '@anticrm/view-resources'
import CreateFilter from './CreateFilter.svelte'
export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates>
const client = getClient()
const hieararchy = client.getHierarchy()
let states: State[] = []
const statesQuery = createQuery()
$: updateStates(space)
function updateStates (space: Ref<SpaceWithStates>): void {
statesQuery.query(
task.class.State,
{ space },
(result) => {
states = result
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
}
let wonStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
const doneStatesQuery = createQuery()
$: updateDoneStates(space)
function updateDoneStates (space: Ref<SpaceWithStates>): void {
doneStatesQuery.query(task.class.DoneState, { space }, (result) => {
wonStates = new Set(result.filter((p) => hieararchy.isDerived(p._class, task.class.WonState)).map((p) => p._id))
})
}
let modified: Timestamp | undefined = undefined
let ids: Ref<Task>[] = []
const txQuery = createQuery()
function updateTxes (_class: Ref<Class<Task>>, space: Ref<SpaceWithStates>, modified: Timestamp | undefined): void {
if (modified === undefined) {
ids = []
return
}
txQuery.query(
core.class.TxCollectionCUD,
{
objectSpace: space,
'tx._class': core.class.TxCreateDoc,
'tx.objectClass': _class,
modifiedOn: { $gte: modified }
},
(result) => {
ids = result.map((p) => p.tx.objectId) as Ref<Task>[]
}
)
}
let items: DashboardItem[] = []
$: updateTxes(_class, space, modified)
const docQuery = createQuery()
$: query = modified
? {
space,
_id: { $in: ids }
}
: { space }
let resultQuery = {
space
}
function updateDocs (_class: Ref<Class<Task>>, states: State[], query: DocumentQuery<Task>): void {
if (states.length === 0) {
return
}
docQuery.query(
_class,
query,
(result) => {
const template: Map<Ref<State>, DashboardItem> = new Map(
states.map((p) => {
return [
p._id,
{
label: p.title,
values: [
{ color: 10, value: 0 },
{ color: 0, value: 0 },
{ color: 11, value: 0 }
]
}
]
})
)
for (const value of result) {
const group = template.get(value.state)
if (group === undefined) continue
if (value.doneState === null) {
group.values[0].value++
} else {
const won = wonStates.has(value.doneState)
if (won === undefined) continue
const index = won ? 1 : 2
group.values[index].value++
}
template.set(value.state, group)
}
items = Array.from(template.values())
},
{
projection: {
state: 1,
doneState: 1
}
}
)
}
$: updateDocs(_class, states, resultQuery)
</script>
<CreateFilter bind:value={modified} />
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
<div class="ml-10 mt-4">
<BarDashboard {items} />
</div>

View File

@ -37,6 +37,7 @@ import TodoItemPresenter from './components/todos/TodoItemPresenter.svelte'
import TodoItemsPopup from './components/todos/TodoItemsPopup.svelte'
import Todos from './components/todos/Todos.svelte'
import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
import Dashboard from './components/Dashboard.svelte'
export { default as AssigneePresenter } from './components/AssigneePresenter.svelte'
@ -52,6 +53,7 @@ export default async (): Promise<Resources> => ({
TaskPresenter,
EditIssue,
KanbanCard,
Dashboard,
TemplatesIcon,
KanbanView,
StatePresenter,

View File

@ -70,7 +70,8 @@ export default mergeIds(taskId, task, {
Tasks: '' as IntlString,
Assigned: '' as IntlString,
Task: '' as IntlString
Task: '' as IntlString,
AllTime: '' as IntlString
},
status: {
AssigneeRequired: '' as IntlString

View File

@ -222,7 +222,8 @@ const task = plugin(taskId, {
ApplicationLabelTask: '' as IntlString,
Projects: '' as IntlString,
ManageProjectStatues: '' as IntlString,
TodoItems: '' as IntlString
TodoItems: '' as IntlString,
Dashboard: '' as IntlString
},
class: {
Issue: '' as Ref<Class<Issue>>,
@ -245,6 +246,7 @@ const task = plugin(taskId, {
},
viewlet: {
Kanban: '' as Ref<ViewletDescriptor>,
Dashboard: '' as Ref<ViewletDescriptor>,
StatusTable: '' as Ref<ViewletDescriptor>
},
icon: {
@ -253,7 +255,8 @@ const task = plugin(taskId, {
TodoCheck: '' as Asset,
TodoUnCheck: '' as Asset,
ManageStatuses: '' as Asset,
TaskState: '' as Asset
TaskState: '' as Asset,
Dashboard: '' as Asset
},
global: {
// Global task root, if not attached to some other object.

View File

@ -105,7 +105,8 @@ export {
BooleanEditor,
BooleanPresenter,
NumberEditor,
NumberPresenter
NumberPresenter,
TimestampPresenter
}
export default async (): Promise<Resources> => ({