UBER-615 HR Schedule UI fixes (#3565)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2023-08-07 18:14:32 +07:00 committed by GitHub
parent eaaef54fa6
commit 32fd80c971
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 29 deletions

View File

@ -308,6 +308,27 @@ export function createModel (builder: Builder): void {
hr.action.EditDepartment hr.action.EditDepartment
) )
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: hr.component.CreateDepartment,
element: 'top',
fillProps: {
_id: 'space'
}
},
label: hr.string.CreateDepartment,
icon: hr.icon.Department,
input: 'focus',
category: hr.category.HR,
target: hr.class.Department,
context: { mode: 'context', application: hr.app.HR, group: 'create' }
},
hr.action.CreateDepartment
)
createAction( createAction(
builder, builder,
{ {
@ -339,7 +360,8 @@ export function createModel (builder: Builder): void {
input: 'any', input: 'any',
category: hr.category.HR, category: hr.category.HR,
target: hr.class.Request, target: hr.class.Request,
context: { mode: 'context', application: hr.app.HR, group: 'create' } context: { mode: 'context', application: hr.app.HR, group: 'create' },
override: [view.action.Open]
}, },
hr.action.EditRequest hr.action.EditRequest
) )
@ -350,7 +372,7 @@ export function createModel (builder: Builder): void {
action: hr.actionImpl.EditRequestType, action: hr.actionImpl.EditRequestType,
actionProps: {}, actionProps: {},
label: hr.string.EditRequestType, label: hr.string.EditRequestType,
icon: view.icon.Open, icon: view.icon.Edit,
input: 'any', input: 'any',
category: hr.category.HR, category: hr.category.HR,
target: hr.class.Request, target: hr.class.Request,

View File

@ -41,6 +41,7 @@ export default mergeIds(hrId, hr, {
}, },
component: { component: {
Structure: '' as AnyComponent, Structure: '' as AnyComponent,
CreateDepartment: '' as AnyComponent,
EditDepartment: '' as AnyComponent, EditDepartment: '' as AnyComponent,
DepartmentStaff: '' as AnyComponent, DepartmentStaff: '' as AnyComponent,
DepartmentEditor: '' as AnyComponent, DepartmentEditor: '' as AnyComponent,
@ -54,6 +55,7 @@ export default mergeIds(hrId, hr, {
HR: '' as Ref<ActionCategory> HR: '' as Ref<ActionCategory>
}, },
action: { action: {
CreateDepartment: '' as Ref<Action>,
EditDepartment: '' as Ref<Action>, EditDepartment: '' as Ref<Action>,
ArchiveDepartment: '' as Ref<Action>, ArchiveDepartment: '' as Ref<Action>,
EditRequest: '' as Ref<Action>, EditRequest: '' as Ref<Action>,

View File

@ -235,7 +235,7 @@
)` )`
divScroll.style.webkitMaskImage = gradient divScroll.style.webkitMaskImage = gradient
} }
if (divHScroll && horizontal) { if (divHScroll && horizontal && !noFade) {
const gradientH = `linear-gradient( const gradientH = `linear-gradient(
90deg, 90deg,
rgba(0, 0, 0, 1) ${shiftLeft}px, rgba(0, 0, 0, 1) ${shiftLeft}px,

View File

@ -250,7 +250,7 @@
{#if rows.length} {#if rows.length}
{@const dep = departmentById.get(department)} {@const dep = departmentById.get(department)}
<Scroller fade={{ multipler: { top: headerHeightRem, bottom: 0, left: headerWidthRem } }} horizontal> <Scroller horizontal fade={{ multipler: { top: headerHeightRem, left: headerWidthRem } }} noFade>
<div bind:clientWidth={containerWidth} class="timeline"> <div bind:clientWidth={containerWidth} class="timeline">
{#key [containerWidthRem, columnWidthRem, headerWidthRem]} {#key [containerWidthRem, columnWidthRem, headerWidthRem]}
<!-- Resource Header --> <!-- Resource Header -->
@ -314,7 +314,14 @@
{#each tracks as track, trackIndex} {#each tracks as track, trackIndex}
{#each track.elements as element} {#each track.elements as element}
{@const request = element.request} {@const request = element.request}
<div class="timeline-event-wrapper" style={getElementStyle(element, trackIndex)}> <div
class="timeline-event-wrapper"
style={getElementStyle(element, trackIndex)}
use:tooltip={{
component: RequestsPopup,
props: { requests: [request._id] }
}}
>
<ScheduleRequest {request} {editable} shouldShowDescription={element.length > 1} /> <ScheduleRequest {request} {editable} shouldShowDescription={element.length > 1} />
</div> </div>
{/each} {/each}

View File

@ -16,7 +16,7 @@
import hr, { Request, RequestType } from '@hcengineering/hr' import hr, { Request, RequestType } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { closeTooltip, Icon, Label, showPopup } from '@hcengineering/ui' import { closeTooltip, Icon, Label, showPopup } from '@hcengineering/ui'
import { ContextMenu, HTMLPresenter } from '@hcengineering/view-resources' import { ContextMenu } from '@hcengineering/view-resources'
export let request: Request export let request: Request
export let editable: boolean = false export let editable: boolean = false
@ -30,10 +30,8 @@
}) })
} }
function isAvailable (type: RequestType, request: Request): boolean { function isAvailable (type: RequestType): boolean {
// TODO Move availability to the Request model return type.value >= 0
const available = type.value >= 0
return available
} }
function click (e: MouseEvent, request: Request) { function click (e: MouseEvent, request: Request) {
@ -43,11 +41,13 @@
closeTooltip() closeTooltip()
showPopup(ContextMenu, { object: request }, e.target as HTMLElement) showPopup(ContextMenu, { object: request }, e.target as HTMLElement)
} }
$: description = shouldShowDescription ? request.description.replace(/<[^>]*>/g, '').trim() : ''
</script> </script>
{#await getType(request) then type} {#await getType(request) then type}
{#if type} {#if type}
{@const available = isAvailable(type, request)} {@const available = isAvailable(type)}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
@ -66,8 +66,8 @@
{#if shouldShowDescription} {#if shouldShowDescription}
<span class="overflow-label"> <span class="overflow-label">
{#if request.description !== ''} {#if description !== ''}
<HTMLPresenter value={request.description} /> {description}
{:else if type} {:else if type}
<Label label={type.label} /> <Label label={type.label} />
{/if} {/if}

View File

@ -16,6 +16,10 @@
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import { Department } from '@hcengineering/hr' import { Department } from '@hcengineering/hr'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import { getActions as getContributedActions } from '@hcengineering/view-resources'
import hr from '../../plugin' import hr from '../../plugin'
@ -27,27 +31,48 @@
export let selected: Ref<Department> | undefined export let selected: Ref<Department> | undefined
export let level = 0 export let level = 0
const client = getClient()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function getDescendants (department: Ref<Department>): Ref<Department>[] { function getDescendants (department: Ref<Department>): Ref<Department>[] {
return (descendants.get(department) ?? []).map((p) => p._id) return (descendants.get(department) ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((p) => p._id)
}
async function getActions (obj: Department): Promise<Action[]> {
const result: Action[] = []
const extraActions = await getContributedActions(client, obj, obj._class)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(obj, evt, act.actionProps)
}
})
}
return result
} }
function handleDepartmentSelected (department: Ref<Department>): void { function handleDepartmentSelected (department: Ref<Department>): void {
dispatch('selected', department) dispatch('selected', department)
} }
$: _departments = departments.map((it) => departmentById.get(it)).filter((it) => it !== undefined) as Department[]
$: _descendants = new Map(_departments.map((it) => [it._id, getDescendants(it._id)]))
</script> </script>
{#each departments as dep} {#each _departments as department}
{@const department = departmentById.get(dep)} {@const desc = _descendants.get(department._id) ?? []}
{@const desc = getDescendants(dep)}
{#if department} {#if department}
<TreeElement <TreeElement
_id={department._id}
icon={hr.icon.Department} icon={hr.icon.Department}
title={department.name} title={department.name}
selected={selected === department._id} selected={selected === department._id}
node={desc.length > 0} node={desc.length > 0}
actions={() => getActions(department)}
{level} {level}
on:click={() => handleDepartmentSelected(department._id)} on:click={() => handleDepartmentSelected(department._id)}
> >

View File

@ -34,19 +34,11 @@
<NavHeader label={hr.string.HRApplication} /> <NavHeader label={hr.string.HRApplication} />
<Scroller shrink> <Scroller shrink>
<!-- TODO Specials -->
<div class="antiNav-divider short line" /> <div class="antiNav-divider short line" />
<TreeNode label={hr.string.Departments} parent> <TreeNode label={hr.string.Departments} parent>
<DepartmentsHierarchy {departments} {descendants} {departmentById} selected={department} on:selected /> <DepartmentsHierarchy {departments} {descendants} {departmentById} selected={department} on:selected />
</TreeNode> </TreeNode>
<!-- TODO Add Positions -->
<!-- <div class="antiNav-divider short line" /> -->
<!-- <TreeNode label={hr.string.Positions} parent> -->
<!-- </TreeNode> -->
</Scroller> </Scroller>
<NavFooter /> <NavFooter />

View File

@ -13,11 +13,14 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Doc, Ref } from '@hcengineering/core'
import type { Asset, IntlString } from '@hcengineering/platform' import type { Asset, IntlString } from '@hcengineering/platform'
import type { AnySvelteComponent } from '@hcengineering/ui' import type { AnySvelteComponent } from '@hcengineering/ui'
import { Icon, IconChevronDown, Label } from '@hcengineering/ui' import { Icon, IconChevronDown, IconMoreH, Label, Menu, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { Action } from '@hcengineering/view'
export let _id: Ref<Doc> | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> | undefined = undefined export let iconProps: Record<string, any> | undefined = undefined
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
@ -27,6 +30,15 @@
export let collapsed = false export let collapsed = false
export let selected = false export let selected = false
export let level = 0 export let level = 0
export let actions: (originalEvent?: MouseEvent) => Promise<Action[]> = async () => []
let hovered = false
async function onMenuClick (ev: MouseEvent) {
showPopup(Menu, { actions: await actions(ev), ctx: _id }, ev.target as HTMLElement, () => {
hovered = false
})
hovered = true
}
$: style = `padding-left: calc(${level} * 1.25rem);` $: style = `padding-left: calc(${level} * 1.25rem);`
@ -36,10 +48,10 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
class="antiNav-element" class="antiNav-element"
class:hovered
class:selected class:selected
class:parent class:parent
class:collapsed class:collapsed
class:child={!node}
{style} {style}
on:click={() => { on:click={() => {
if (selected) { if (selected) {
@ -59,12 +71,25 @@
</span> </span>
{#if node} {#if node}
<div class="an-element__icon-arrow" class:collapsed> <div
class="an-element__icon-arrow"
class:collapsed
on:click={(e) => {
e.stopPropagation()
e.preventDefault()
collapsed = !collapsed
}}
>
<IconChevronDown size={'small'} /> <IconChevronDown size={'small'} />
</div> </div>
{/if} {/if}
</span> </span>
<div class="an-element__tool" on:click|preventDefault|stopPropagation={onMenuClick}>
<IconMoreH size={'small'} />
</div>
</div> </div>
{#if node && !collapsed} {#if node && !collapsed}
<div class="antiNav-element__dropbox"><slot /></div> <div class="antiNav-element__dropbox"><slot /></div>
{/if} {/if}

View File

@ -14,6 +14,7 @@
// //
import { Resources } from '@hcengineering/platform' import { Resources } from '@hcengineering/platform'
import CreateDepartment from './components/CreateDepartment.svelte'
import DepartmentEditor from './components/DepartmentEditor.svelte' import DepartmentEditor from './components/DepartmentEditor.svelte'
import DepartmentStaff from './components/DepartmentStaff.svelte' import DepartmentStaff from './components/DepartmentStaff.svelte'
import EditDepartment from './components/EditDepartment.svelte' import EditDepartment from './components/EditDepartment.svelte'
@ -34,6 +35,7 @@ async function editRequestType (object: Request): Promise<void> {
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
Structure, Structure,
CreateDepartment,
EditDepartment, EditDepartment,
DepartmentStaff, DepartmentStaff,
DepartmentEditor, DepartmentEditor,