Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-06-20 21:59:56 +06:00 committed by GitHub
parent 09130c5b69
commit 1091ced274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 214 additions and 217 deletions

View File

@ -34,6 +34,8 @@
"@anticrm/model-view": "~0.6.0",
"@anticrm/model-workbench": "~0.6.1",
"@anticrm/model-contact": "~0.6.1",
"@anticrm/model-chunter": "~0.6.0",
"@anticrm/model-attachment": "~0.6.0",
"@anticrm/hr": "~0.6.0",
"@anticrm/hr-resources": "~0.6.0",
"@anticrm/view": "~0.6.0"

View File

@ -14,14 +14,16 @@
//
import { Employee } from '@anticrm/contact'
import contact, { TEmployee } from '@anticrm/model-contact'
import { IndexKind, Ref } from '@anticrm/core'
import type { Department, Staff } from '@anticrm/hr'
import { Builder, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import contact, { TEmployee, TEmployeeAccount } from '@anticrm/model-contact'
import { Arr, IndexKind, Ref } from '@anticrm/core'
import type { Department, DepartmentMember, Staff } from '@anticrm/hr'
import { Builder, Index, Mixin, Model, Prop, TypeRef, Collection, TypeString, UX, ArrOf } from '@anticrm/model'
import core, { TSpace } from '@anticrm/model-core'
import workbench from '@anticrm/model-workbench'
import hr from './plugin'
import view, { createAction } from '@anticrm/model-view'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
@Model(hr.class.Department, core.class.Space)
@UX(hr.string.Department, hr.icon.Department)
@ -33,12 +35,28 @@ export class TDepartment extends TSpace implements Department {
@Index(IndexKind.FullText)
name!: string
@Prop(Collection(contact.class.Channel), contact.string.ContactInfo)
channels?: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, undefined, attachment.string.Files)
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
avatar?: string | null
@Prop(TypeRef(contact.class.Employee), hr.string.TeamLead)
teamLead!: Ref<Employee> | null
@Prop(ArrOf(TypeRef(hr.class.DepartmentMember)), contact.string.Members)
declare members: Arr<Ref<DepartmentMember>>
}
@Model(hr.class.DepartmentMember, contact.class.EmployeeAccount)
@UX(contact.string.Employee, hr.icon.HR)
export class TDepartmentMember extends TEmployeeAccount implements DepartmentMember {}
@Mixin(hr.mixin.Staff, contact.class.Employee)
@UX(contact.string.Employee, hr.icon.HR)
export class TStaff extends TEmployee implements Staff {
@ -47,7 +65,7 @@ export class TStaff extends TEmployee implements Staff {
}
export function createModel (builder: Builder): void {
builder.createModel(TDepartment, TStaff)
builder.createModel(TDepartment, TDepartmentMember, TStaff)
builder.createDoc(
workbench.class.Application,
@ -76,6 +94,14 @@ export function createModel (builder: Builder): void {
inlineEditor: hr.component.DepartmentEditor
})
builder.mixin(hr.class.Department, core.class.Class, view.mixin.ObjectEditor, {
editor: hr.component.EditDepartment
})
builder.mixin(hr.class.DepartmentMember, core.class.Class, view.mixin.ArrayEditor, {
editor: hr.component.DepartmentStaff
})
createAction(
builder,
{

View File

@ -118,7 +118,8 @@ export class TCollectionEditor extends TClass implements CollectionEditor {
@Mixin(view.mixin.ArrayEditor, core.class.Class)
export class TArrayEditor extends TClass implements ArrayEditor {
inlineEditor!: AnyComponent
inlineEditor?: AnyComponent
editor?: AnyComponent
}
@Mixin(view.mixin.AttributePresenter, core.class.Class)

View File

@ -55,6 +55,9 @@
const typeClass = hierarchy.getClass(presenterClass.attrClass)
const editorMixin = hierarchy.as(typeClass, mixinRef)
if (category === 'array' && editorMixin.inlineEditor === undefined) {
return
}
editor = getResource(editorMixin.inlineEditor).catch((cause) => {
console.error(`failed to find editor for ${_class} ${attribute} ${presenterClass.attrClass} cause: ${cause}`)
})

View File

@ -38,6 +38,7 @@
"@anticrm/core": "~0.6.16",
"@anticrm/panel": "~0.6.0",
"@anticrm/contact": "~0.6.5",
"@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/contact-resources": "~0.6.0",
"@anticrm/setting": "~0.6.1",

View File

@ -19,25 +19,19 @@
import CreateDepartment from './CreateDepartment.svelte'
import DepartmentCard from './DepartmentCard.svelte'
import hr from '../plugin'
import { IconAdd, IconMoreV, Button, eventToHTMLElement, Label, showPopup, ActionIcon } from '@anticrm/ui'
import { IconAdd, IconMoreV, Button, eventToHTMLElement, Label, showPopup, ActionIcon, showPanel } from '@anticrm/ui'
import contact, { Employee } from '@anticrm/contact'
import { EmployeePresenter } from '@anticrm/contact-resources'
import DepartmentStaff from './DepartmentStaff.svelte'
import { Menu } from '@anticrm/view-resources'
import view from '@anticrm/view'
export let value: WithLookup<Department>
export let descendants: Map<Ref<Department>, WithLookup<Department>[]>
$: currentDescendants = descendants.get(value._id) ?? []
let expand = false
const client = getClient()
function toggle () {
if (currentDescendants.length === 0) return
expand = !expand
}
async function changeLead (result: Employee | null | undefined): Promise<void> {
if (result === undefined) {
return
@ -50,6 +44,8 @@
}
function openLeadEditor (event: MouseEvent) {
event?.preventDefault()
event?.stopPropagation()
showPopup(
UsersPopup,
{
@ -64,13 +60,7 @@
}
function createChild (e: MouseEvent) {
showPopup(CreateDepartment, { space: value._id }, eventToHTMLElement(e), (res) => {
if (res && !expand) expand = true
})
}
function editMembers (e: MouseEvent) {
showPopup(DepartmentStaff, { _id: value._id }, 'float')
showPopup(CreateDepartment, { space: value._id }, eventToHTMLElement(e))
}
function showMenu (e: MouseEvent) {
@ -82,29 +72,30 @@
}
)
}
function edit (e: MouseEvent): void {
showPanel(view.component.EditDoc, value._id, value._class, 'content')
}
</script>
<div class="flex-center w-full px-4">
<div
class="w-full mt-2 mb-2 container flex"
class:cursor-pointer={currentDescendants.length}
on:click|stopPropagation={toggle}
on:click|stopPropagation={edit}
on:contextmenu|preventDefault={showMenu}
>
{#if currentDescendants.length}
<div class="verticalDivider" />
<div class="verticalDivider" />
{/if}
<div class="flex-between pt-4 pb-4 pr-4 pl-2 w-full">
<div class="flex-center">
<div class="mr-2">
<Button icon={IconAdd} on:click={createChild} />
</div>
<Avatar size={'medium'} avatar={value.avatar} icon={hr.icon.Department} />
<div class="flex-row ml-2">
<div class="fs-title">
{value.name}
</div>
<div class="cursor-pointer" on:click|stopPropagation={editMembers}>
<Label label={hr.string.MemberCount} params={{ count: value.members.length }} />
</div>
<Label label={hr.string.MemberCount} params={{ count: value.members.length }} />
</div>
</div>
<div class="flex-center mr-2">
@ -122,19 +113,16 @@
onEmployeeEdit={openLeadEditor}
/>
</div>
<Button icon={IconAdd} on:click={createChild} />
<ActionIcon icon={IconMoreV} size={'medium'} action={showMenu} />
</div>
</div>
</div>
</div>
{#if expand && currentDescendants.length}
<div class="ml-8">
{#each descendants.get(value._id) ?? [] as nested}
<DepartmentCard value={nested} {descendants} />
{/each}
</div>
{/if}
<div class="ml-8">
{#each currentDescendants as nested}
<DepartmentCard value={nested} {descendants} />
{/each}
</div>
<style lang="scss">
.container {

View File

@ -21,7 +21,7 @@
import hr from '../plugin'
export let value: Ref<Department> | undefined
export let label: IntlString = hr.string.Department
export let label: IntlString = hr.string.ParentDepartmentLabel
export let onChange: (value: any) => void
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
@ -31,7 +31,7 @@
<SpaceSelector
_class={hr.class.Department}
label={hr.string.ParentDepartmentLabel}
{label}
{size}
{kind}
{justify}

View File

@ -13,33 +13,31 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
import { Employee } from '@anticrm/contact'
import { EmployeePresenter } from '@anticrm/contact-resources'
import contact from '@anticrm/contact-resources/src/plugin'
import { Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { Department, Staff } from '@anticrm/hr'
import { Avatar, createQuery, MessageBox, getClient, UsersPopup } from '@anticrm/presentation'
import { Scroller, Panel, Button, showPopup, eventToHTMLElement } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import StaffPresenter from './StaffPresenter.svelte'
import { Department, DepartmentMember, Staff } from '@anticrm/hr'
import { createQuery, getClient, MessageBox, UsersPopup } from '@anticrm/presentation'
import { CircleButton, eventToHTMLElement, IconAdd, Label, Scroller, showPopup } from '@anticrm/ui'
import hr from '../plugin'
export let _id: Ref<Department> | undefined
export let objectId: Ref<Department> | undefined
let value: Department | undefined
let employees: WithLookup<Staff>[] = []
let accounts: EmployeeAccount[] = []
const dispatch = createEventDispatcher()
let accounts: DepartmentMember[] = []
const departmentQuery = createQuery()
const query = createQuery()
const accountsQuery = createQuery()
const client = getClient()
$: _id &&
$: objectId &&
value === undefined &&
departmentQuery.query(
hr.class.Department,
{
_id
_id: objectId
},
(res) => ([value] = res)
)
@ -48,7 +46,7 @@
accountsQuery.query(
contact.class.EmployeeAccount,
{
_id: { $in: value.members as Ref<EmployeeAccount>[] }
_id: { $in: value.members }
},
(res) => {
accounts = res
@ -79,7 +77,7 @@
UsersPopup,
{
_class: contact.class.Employee,
ignoreUsers: employees.filter((p) => p.department === _id).map((p) => p._id)
ignoreUsers: employees.filter((p) => p.department === objectId).map((p) => p._id)
},
eventToHTMLElement(e),
addMember
@ -87,7 +85,7 @@
}
async function addMember (employee: Employee | undefined): Promise<void> {
if (employee === undefined || value === undefined) {
if (employee === null || employee === undefined || value === undefined) {
return
}
@ -131,32 +129,64 @@
}
</script>
<Panel
isHeader={true}
isAside={false}
isFullSize
on:fullsize
on:close={() => {
dispatch('close')
}}
>
<svelte:fragment slot="title">
<div class="antiTitle icon-wrapper">
{#if value}
<div class="wrapped-icon"><Avatar size={'medium'} avatar={value.avatar} icon={hr.icon.Department} /></div>
<div class="title-wrapper">
<span class="wrapped-title">{value.name}</span>
</div>
{/if}
<div class="container">
<div class="flex flex-between">
<div class="title"><Label label={contact.string.Members} /></div>
<CircleButton id={hr.string.AddEmployee} icon={IconAdd} size={'small'} selected on:click={add} />
</div>
{#if employees.length > 0}
<Scroller>
<table class="antiTable">
<thead class="scroller-thead">
<tr class="scroller-thead__tr">
<th><Label label={contact.string.Member} /></th>
<th><Label label={hr.string.Department} /></th>
</tr>
</thead>
<tbody>
{#each employees as value}
<tr class="antiTable-body__row">
<td><EmployeePresenter {value} /></td>
<td>
{#if value.$lookup?.department}
{value.$lookup.department.name}
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</Scroller>
{:else}
<div class="flex-col-center mt-5 create-container">
<div class="text-sm content-dark-color mt-2">
<Label label={contact.string.NoMembers} />
</div>
<div class="text-sm">
<div class="over-underline" on:click={add}><Label label={contact.string.AddMember} /></div>
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="utils">
<Button label={hr.string.AddEmployee} kind={'primary'} on:click={add} />
</svelte:fragment>
{/if}
</div>
<Scroller>
{#each employees as value}
<StaffPresenter {value} />
{/each}
</Scroller>
</Panel>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
.title {
margin-right: 0.75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
}
.create-container {
padding: 1rem;
color: var(--theme-caption-color);
background: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-radius: 0.75rem;
}
</style>

View File

@ -14,11 +14,9 @@
-->
<script lang="ts">
import { createQuery, EditableAvatar, getClient } from '@anticrm/presentation'
import { Panel } from '@anticrm/panel'
import { createFocusManager, EditBox, FocusHandler } from '@anticrm/ui'
import { ActionContext } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onMount } from 'svelte'
import { Department } from '@anticrm/hr'
import core, { getCurrentAccount, Ref, Space } from '@anticrm/core'
import hr from '../plugin'
@ -27,20 +25,10 @@
import { ChannelsEditor } from '@anticrm/contact-resources'
import setting, { IntegrationType } from '@anticrm/setting'
export let _id: Ref<Department>
let object: Department | undefined
export let object: Department
const dispatch = createEventDispatcher()
const client = getClient()
const query = createQuery()
query.query(
hr.class.Department,
{ _id },
(res) => {
object = res[0]
},
{ limit: 1 }
)
async function onAvatarDone (e: any) {
if (object === undefined) return
@ -77,10 +65,6 @@
const manager = createFocusManager()
const _update = (result: any): void => {
dispatch('update', result)
}
let integrations: Set<Ref<IntegrationType>> = new Set<Ref<IntegrationType>>()
const accountId = getCurrentAccount()._id
const settingsQuery = createQuery()
@ -91,63 +75,46 @@
integrations = new Set(res.map((p) => p.type))
}
)
</script>
<ActionContext
context={{
mode: 'editor'
}}
/>
onMount(() => {
dispatch('open', {
ignoreKeys: ['comments', 'name', 'channels', 'private', 'archived'],
collectionArrays: ['members']
})
})
</script>
<FocusHandler {manager} />
{#if object !== undefined}
<Panel
icon={hr.icon.Department}
title={object.name}
{object}
isHeader={false}
isAside={true}
on:update={(ev) => _update(ev.detail)}
on:close={() => {
dispatch('close')
}}
>
<div class="flex-row-stretch flex-grow">
<div class="mr-8">
{#key object}
<EditableAvatar
avatar={object.avatar}
size={'x-large'}
icon={hr.icon.Department}
on:done={onAvatarDone}
on:remove={removeAvatar}
/>
{/key}
<div class="flex-row-stretch flex-grow">
<div class="mr-8">
{#key object}
<EditableAvatar
avatar={object.avatar}
size={'x-large'}
icon={hr.icon.Department}
on:done={onAvatarDone}
on:remove={removeAvatar}
/>
{/key}
</div>
<div class="flex-grow flex-col">
<div class="name">
<EditBox
placeholder={core.string.Name}
maxWidth="20rem"
bind:value={object.name}
on:change={nameChange}
focusIndex={1}
/>
</div>
<div class="flex-grow flex-col">
<div class="name">
<EditBox
placeholder={core.string.Name}
maxWidth="20rem"
bind:value={object.name}
on:change={nameChange}
focusIndex={1}
/>
</div>
<div class="separator" />
<div class="flex-row-center">
<ChannelsEditor
attachedTo={object._id}
attachedClass={object._class}
{integrations}
focusIndex={10}
on:click
/>
</div>
<div class="separator" />
<div class="flex-row-center">
<ChannelsEditor attachedTo={object._id} attachedClass={object._class} {integrations} focusIndex={10} on:click />
</div>
</div>
</Panel>
</div>
{/if}
<style lang="scss">

View File

@ -1,36 +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 { formatName } from '@anticrm/contact'
import { WithLookup } from '@anticrm/core'
import { Staff } from '@anticrm/hr'
import { Avatar } from '@anticrm/presentation'
export let value: WithLookup<Staff>
</script>
<div class="flex-between w-full p-4">
<div class="flex-row-center">
<Avatar avatar={value.avatar} size={'medium'} />
<div class="fs-title ml-2">
{formatName(value.name)}
</div>
</div>
<div>
{#if value.$lookup?.department}
{value.$lookup.department.name}
{/if}
</div>
</div>

View File

@ -13,8 +13,8 @@
// limitations under the License.
//
import type { Employee } from '@anticrm/contact'
import type { Class, Doc, Mixin, Ref, Space } from '@anticrm/core'
import type { Employee, EmployeeAccount } from '@anticrm/contact'
import type { Arr, Class, Doc, Mixin, Ref, Space } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
@ -25,8 +25,17 @@ export interface Department extends Space {
space: Ref<Department>
avatar?: string | null
teamLead: Ref<Employee> | null
attachments?: number
comments?: number
channels?: number
members: Arr<Ref<DepartmentMember>>
}
/**
* @public
*/
export interface DepartmentMember extends EmployeeAccount {}
/**
* @public
*/
@ -47,7 +56,8 @@ const hr = plugin(hrId, {
HR: '' as Ref<Doc>
},
class: {
Department: '' as Ref<Class<Department>>
Department: '' as Ref<Class<Department>>,
DepartmentMember: '' as Ref<Class<DepartmentMember>>
},
mixin: {
Staff: '' as Ref<Mixin<Staff>>

View File

@ -22,23 +22,7 @@
export let allowedCollections: string[] = []
</script>
<ClassAttributeBar
_class={object._class}
{object}
{ignoreKeys}
to={undefined}
{allowedCollections}
vertical
on:update
/>
<ClassAttributeBar _class={object._class} {object} {ignoreKeys} to={undefined} {allowedCollections} on:update />
{#each mixins as mixin}
<ClassAttributeBar
_class={mixin._id}
{object}
{ignoreKeys}
to={object._class}
{allowedCollections}
vertical
on:update
/>
<ClassAttributeBar _class={mixin._id} {object} {ignoreKeys} to={object._class} {allowedCollections} on:update />
{/each}

View File

@ -91,6 +91,7 @@
let ignoreKeys: string[] = []
let allowedCollections: string[] = []
let collectionArrays: string[] = []
let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
async function updateKeys (): Promise<void> {
@ -102,12 +103,14 @@
}
}
const filtredKeys = Array.from(keysMap.values())
keys = collectionsFilter(hierarchy, filtredKeys, false)
keys = collectionsFilter(hierarchy, filtredKeys, false, allowedCollections)
const collectionKeys = collectionsFilter(hierarchy, filtredKeys, true)
const collectionKeys = collectionsFilter(hierarchy, filtredKeys, true, collectionArrays)
const editors: { key: KeyedAttribute; editor: AnyComponent }[] = []
for (const k of collectionKeys) {
if (allowedCollections.includes(k.key)) continue
const editor = await getCollectionEditor(k)
if (editor === undefined) continue
editors.push({ key: k, editor })
}
collectionEditors = editors
@ -129,10 +132,11 @@
updateKeys()
}
async function getCollectionEditor (key: KeyedAttribute): Promise<AnyComponent> {
async function getCollectionEditor (key: KeyedAttribute): Promise<AnyComponent | undefined> {
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
const clazz = hierarchy.getClass(attrClass.attrClass)
const editorMixin = hierarchy.as(clazz, view.mixin.CollectionEditor)
const mixinRef = attrClass.category === 'array' ? view.mixin.ArrayEditor : view.mixin.CollectionEditor
const editorMixin = hierarchy.as(clazz, mixinRef)
return editorMixin.editor
}
@ -243,7 +247,13 @@
on:update={updateKeys}
/>
{:else if dir === 'column'}
<DocAttributeBar {object} {mixins} {ignoreKeys} {allowedCollections} on:update={updateKeys} />
<DocAttributeBar
{object}
{mixins}
ignoreKeys={[...ignoreKeys, ...collectionArrays]}
{allowedCollections}
on:update={updateKeys}
/>
{:else}
<AttributesBar {object} _class={realObjectClass} {keys} />
{/if}
@ -258,12 +268,13 @@
ignoreKeys = ev.detail.ignoreKeys
ignoreMixins = new Set(ev.detail.ignoreMixins)
allowedCollections = ev.detail.allowedCollections ?? []
collectionArrays = ev.detail.collectionArrays ?? []
getMixins(parentClass, object)
updateKeys()
}}
/>
{/if}
{#each collectionEditors.filter((it) => !allowedCollections.includes(it.key.key)) as collection}
{#each collectionEditors as collection}
{#if collection.editor}
<div class="mt-6">
<Component

View File

@ -381,10 +381,19 @@ export function getFiltredKeys (
return filterKeys(hierarchy, keys, ignoreKeys)
}
export function collectionsFilter (hierarchy: Hierarchy, keys: KeyedAttribute[], get: boolean): KeyedAttribute[] {
export function collectionsFilter (
hierarchy: Hierarchy,
keys: KeyedAttribute[],
get: boolean,
include: string[]
): KeyedAttribute[] {
const result: KeyedAttribute[] = []
for (const key of keys) {
if (isCollectionAttr(hierarchy, key) === get) result.push(key)
if (include.includes(key.key)) {
result.push(key)
} else if (isCollectionAttr(hierarchy, key) === get) {
result.push(key)
}
}
return result
}

View File

@ -99,7 +99,8 @@ export interface CollectionEditor extends Class<Doc> {
* @public
*/
export interface ArrayEditor extends Class<Doc> {
inlineEditor: AnyComponent
editor?: AnyComponent
inlineEditor?: AnyComponent
}
/**

View File

@ -14,8 +14,8 @@
//
import contact, { Employee } from '@anticrm/contact'
import core, { Account, Ref, SortingOrder, Tx, TxFactory, TxMixin } from '@anticrm/core'
import hr, { Department, Staff } from '@anticrm/hr'
import core, { Ref, SortingOrder, Tx, TxFactory, TxMixin } from '@anticrm/core'
import hr, { Department, DepartmentMember, Staff } from '@anticrm/hr'
import { extractTx, TriggerControl } from '@anticrm/server-core'
async function getOldDepartment (
@ -67,7 +67,7 @@ function exlude (first: Ref<Department>[], second: Ref<Department>[]): Ref<Depar
function getTxes (
factory: TxFactory,
account: Ref<Account>,
account: Ref<DepartmentMember>,
added: Ref<Department>[],
removed?: Ref<Department>[]
): Tx[] {