UBERF-5827: add collaborative description for companies (#4851)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-03-06 12:01:05 +04:00 committed by GitHub
parent e003fb29fb
commit 1a1b978f82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 365 additions and 77 deletions

View File

@ -40,7 +40,8 @@ import {
type Class,
type Domain,
type Ref,
type Timestamp
type Timestamp,
type Markup
} from '@hcengineering/core'
import {
Collection,
@ -57,7 +58,8 @@ import {
TypeString,
TypeTimestamp,
UX,
type Builder
type Builder,
TypeCollaborativeMarkup
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
@ -156,6 +158,10 @@ export class TMember extends TAttachedDoc implements Member {
@Model(contact.class.Organization, contact.class.Contact)
@UX(contact.string.Organization, contact.icon.Company, 'ORG', 'name')
export class TOrganization extends TContact implements Organization {
@Prop(TypeCollaborativeMarkup(), core.string.Description)
@Index(IndexKind.FullText)
description?: Markup
@Prop(Collection(contact.class.Member), contact.string.Members)
members!: number
}
@ -771,6 +777,29 @@ export function createModel (builder: Builder): void {
filters: []
})
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectPanel, {
component: contact.component.EditOrganizationPanel
})
createAction(builder, {
label: view.string.Open,
icon: view.icon.Open,
action: view.actionImpl.ShowPanel,
actionProps: {
component: contact.component.EditOrganizationPanel,
element: 'content'
},
input: 'focus',
category: contact.category.Contact,
override: [view.action.Open],
keyBinding: ['keyE'],
target: contact.class.Organization,
context: {
mode: ['context', 'browser'],
group: 'create'
}
})
builder.mixin(contact.class.Channel, core.class.Class, view.mixin.AttributeFilter, {
component: contact.component.ChannelFilter
})

View File

@ -70,10 +70,6 @@ export function createReviewModel (builder: Builder): void {
presenter: recruit.component.OpinionPresenter
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditReview
})
createAction(builder, {
action: view.actionImpl.ShowPopup,
actionProps: {

View File

@ -14,10 +14,13 @@
-->
<script lang="ts">
import { Channel, findContacts, Organization } from '@hcengineering/contact'
import { AttachedData, fillDefaults, generateId, Ref, TxOperations, WithLookup } from '@hcengineering/core'
import core, { AttachedData, fillDefaults, generateId, Ref, TxOperations, WithLookup } from '@hcengineering/core'
import { Card, getClient, InlineAttributeBar } from '@hcengineering/presentation'
import { Button, createFocusManager, EditBox, FocusHandler, IconInfo, Label } from '@hcengineering/ui'
import { Button, createFocusManager, EditBox, FocusHandler, IconAttachment, IconInfo, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { AttachmentPresenter, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Attachment } from '@hcengineering/attachment'
import contact from '../plugin'
import ChannelsDropdown from './ChannelsDropdown.svelte'
import Company from './icons/Company.svelte'
@ -32,7 +35,9 @@
const id: Ref<Organization> = generateId()
const object: Organization = {
name: ''
name: '',
description: '',
attachments: 0
} as unknown as Organization
const dispatch = createEventDispatcher()
@ -41,8 +46,10 @@
fillDefaults(hierarchy, object, contact.class.Organization)
async function createOrganization () {
async function createOrganization (): Promise<void> {
await client.createDoc(contact.class.Organization, contact.space.Contacts, object, id)
await descriptionBox.createAttachments(id)
for (const channel of channels) {
await client.addCollection(
contact.class.Channel,
@ -69,10 +76,14 @@
let matches: WithLookup<Organization>[] = []
let matchedChannels: AttachedData<Channel>[] = []
$: findContacts(client, contact.class.Organization, object.name, channels).then((p) => {
$: void findContacts(client, contact.class.Organization, object.name, channels).then((p) => {
matches = p.contacts as Organization[]
matchedChannels = p.channels
})
let descriptionBox: AttachmentStyledBox
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
</script>
<FocusHandler {manager} />
@ -80,13 +91,14 @@
<Card
label={contact.string.CreateOrganization}
okAction={createOrganization}
hideAttachments={attachments.size === 0}
canSave={object.name.length > 0}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<div class="flex-row-center clear-mins">
<div class="flex-row-center clear-mins mb-3">
<div class="mr-3">
<Button icon={Company} size={'medium'} kind={'link-bordered'} noFocus />
</div>
@ -98,6 +110,29 @@
focusIndex={1}
/>
</div>
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={id}
_class={contact.class.Organization}
space={contact.space.Contacts}
alwaysEdit
showButtons={false}
bind:content={object.description}
placeholder={core.string.Description}
kind="indented"
isScrollable={false}
enableBackReferences={true}
enableAttachments={false}
on:attachments={(ev) => {
if (ev.detail.size > 0) attachments = ev.detail.values
else if (ev.detail.size === 0 && ev.detail.values != null) {
attachments.clear()
attachments = attachments
}
}}
/>
<svelte:fragment slot="pool">
<ChannelsDropdown
bind:value={channels}
@ -116,7 +151,30 @@
extraProps={{ showNavigate: false }}
/>
</svelte:fragment>
<svelte:fragment slot="attachments">
{#if attachments.size > 0}
{#each Array.from(attachments.values()) as attachment}
<AttachmentPresenter
value={attachment}
showPreview
removable
on:remove={(result) => {
if (result.detail !== undefined) descriptionBox.removeAttachmentById(result.detail._id)
}}
/>
{/each}
{/if}
</svelte:fragment>
<svelte:fragment slot="footer">
<Button
icon={IconAttachment}
size="large"
on:click={() => {
descriptionBox.handleAttach()
}}
/>
{#if matches.length > 0}
<div class="flex-row-center error-color">
<IconInfo size={'small'} />

View File

@ -0,0 +1,163 @@
<script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import core, { Doc, Mixin, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, {
type AttributeCategory,
createQuery,
getClient,
type KeyedAttribute
} from '@hcengineering/presentation'
import { type AnyComponent, Button, Component, IconMixin, IconMoreH, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
DocAttributeBar,
DocNavLink,
getCollectionCounter,
getDocAttrsInfo,
getDocMixins,
showMenu
} from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import { Organization } from '@hcengineering/contact'
import contact from '../plugin'
import EditOrganization from './EditOrganization.svelte'
export let _id: Ref<Organization>
export let embedded: boolean = false
export let readonly: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
const dispatch = createEventDispatcher()
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
const ignoreKeys = ['comments', 'name', 'channels', 'description', 'attachments']
let object: Organization | undefined = undefined
let lastId: Ref<Organization> | undefined = undefined
let mixins: Mixin<Doc>[] = []
let editors: Array<{ key: KeyedAttribute, editor: AnyComponent, category: AttributeCategory }> = []
let showAllMixins = false
let saved = false
$: mixins = object ? getDocMixins(object, showAllMixins) : []
$: descriptionKey = client.getHierarchy().getAttribute(contact.class.Organization, 'description')
$: getDocAttrsInfo(mixins, ignoreKeys, contact.class.Organization).then((res) => {
editors = res.editors
})
$: updateObject(_id)
function updateObject (_id: Ref<Organization>): void {
if (lastId !== _id) {
const prev = lastId
lastId = _id
if (prev !== undefined) {
void inboxClient.then((client) => client.readDoc(getClient(), prev))
}
query.query(contact.class.Organization, { _id }, (result) => {
object = result[0]
})
}
}
onDestroy(async () => {
void inboxClient.then((client) => client.readDoc(getClient(), _id))
})
</script>
{#if object}
<Panel
isHeader={false}
isSub={false}
isAside={true}
{embedded}
{object}
on:open
on:close={() => {
dispatch('close')
}}
>
<svelte:fragment slot="title">
<DocNavLink noUnderline {object}>
<div class="title">{object.name}</div>
</DocNavLink>
</svelte:fragment>
<svelte:fragment slot="attributes" let:direction={dir}>
{#if dir === 'column'}
<DocAttributeBar {object} {mixins} {ignoreKeys} />
{/if}
</svelte:fragment>
<svelte:fragment slot="pre-utils">
{#if saved}
<Label label={presentation.string.Saved} />
{/if}
</svelte:fragment>
<svelte:fragment slot="utils">
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
<Button
icon={IconMixin}
kind={'icon'}
iconProps={{ size: 'medium' }}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
/>
</svelte:fragment>
<div class="flex-col flex-grow flex-no-shrink step-tb-6">
<EditOrganization {object} />
<div class="flex-col flex-grow w-full mt-6 relative">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
placeholder={core.string.Description}
on:saved={(evt) => {
saved = evt.detail
}}
/>
</div>
</div>
{#each editors as editor}
{#if editor.editor}
<div class="step-tb-6">
<Component
is={editor.editor}
props={{
objectId: object._id,
_class: editor.key.attr.attributeOf,
object,
space: object.space,
key: editor.key,
readonly,
[editor.key.key]: getCollectionCounter(hierarchy, object, editor.key)
}}
/>
</div>
{/if}
{/each}
</Panel>
{/if}

View File

@ -18,6 +18,8 @@
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip } from '@hcengineering/ui'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import contact from '../plugin'
import Company from './icons/Company.svelte'
export let value: Organization
@ -30,9 +32,15 @@
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
<ObjectMention
object={value}
{disabled}
{accent}
{noUnderline}
component={contact.component.EditOrganizationPanel}
/>
{:else}
<DocNavLink {disabled} object={value} {accent} {noUnderline}>
<DocNavLink {disabled} object={value} {accent} {noUnderline} component={contact.component.EditOrganizationPanel}>
<div class="flex-presenter" style:max-width={maxWidth} use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
<div class="icon circle">
<Company size={'small'} />

View File

@ -103,6 +103,7 @@ import UsersList from './components/UsersList.svelte'
import SelectUsersPopup from './components/SelectUsersPopup.svelte'
import IconAddMember from './components/icons/AddMember.svelte'
import UserDetails from './components/UserDetails.svelte'
import EditOrganizationPanel from './components/EditOrganizationPanel.svelte'
import contact from './plugin'
import {
@ -332,7 +333,8 @@ export default async (): Promise<Resources> => ({
PersonAccountFilterValuePresenter,
DeleteConfirmationPopup,
PersonAccountRefPresenter,
PersonIcon
PersonIcon,
EditOrganizationPanel
},
completion: {
EmployeeQuery: async (

View File

@ -37,7 +37,8 @@ import {
type Timestamp,
type TxOperations,
getCurrentAccount,
toIdMap
toIdMap,
type Class
} from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
@ -268,14 +269,18 @@ async function generateLocation (loc: Location, id: Ref<Contact>): Promise<Resol
: client.getHierarchy().isDerived(doc._class, contact.mixin.Employee)
? 'employees'
: 'persons'
const objectPanel = client.getHierarchy().classHierarchyMixin(doc._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
const component = objectPanel?.component ?? view.component.EditDoc
return {
loc: {
path: [appComponent, workspace],
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
fragment: getPanelURI(component, doc._id, doc._class, 'content')
},
defaultLocation: {
path: [appComponent, workspace, contactId, special],
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
fragment: getPanelURI(component, doc._id, doc._class, 'content')
}
}
}

View File

@ -121,6 +121,7 @@ export interface Member extends AttachedDoc {
*/
export interface Organization extends Contact {
members: number
description?: string
}
/**
@ -196,7 +197,8 @@ export const contactPlugin = plugin(contactId, {
SpaceMembers: '' as AnyComponent,
DeleteConfirmationPopup: '' as AnyComponent,
AccountArrayEditor: '' as AnyComponent,
PersonIcon: '' as AnyComponent
PersonIcon: '' as AnyComponent,
EditOrganizationPanel: '' as AnyComponent
},
channelProvider: {
Email: '' as Ref<ChannelProvider>,

View File

@ -72,7 +72,8 @@ async function generateIdLocation (loc: Location, shortLink: string): Promise<Re
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
const objectPanel = hierarchy.classHierarchyMixin(recruit.mixin.Candidate as Ref<Class<Doc>>, view.mixin.ObjectPanel)
const objectPanel = hierarchy.classHierarchyMixin(Hierarchy.mixinOrClass(doc), view.mixin.ObjectPanel)
const component = objectPanel?.component ?? view.component.EditDoc
const special = _class === recruit.mixin.Candidate ? 'talents' : 'organizations'
const defaultPath = [appComponent, workspace, recruitId, special]

View File

@ -21,11 +21,9 @@
import {
ActionContext,
AttributeCategory,
AttributeCategoryOrder,
AttributesBar,
KeyedAttribute,
createQuery,
getAttributePresenterClass,
getClient,
hasResource
} from '@hcengineering/presentation'
@ -33,8 +31,8 @@
import view from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte'
import { DocNavLink, ParentsNavigator, getDocLabel, getDocMixins, showMenu } from '..'
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
import { DocNavLink, ParentsNavigator, getDocLabel, getDocMixins, showMenu, getDocAttrsInfo } from '..'
import { getCollectionCounter } from '../utils'
import DocAttributeBar from './DocAttributeBar.svelte'
export let _id: Ref<Doc>
@ -96,8 +94,6 @@
let mixins: Array<Mixin<Doc>> = []
let showAllMixins = false
$: mixins = getDocMixins(object, showAllMixins, ignoreMixins, realObjectClass)
const dispatch = createEventDispatcher()
let ignoreKeys: string[] = []
@ -107,33 +103,14 @@
let inplaceAttributes: string[] = []
let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
$: mixins = getDocMixins(object, showAllMixins, ignoreMixins, realObjectClass)
async function updateKeys (): Promise<void> {
const keysMap = new Map(getFiltredKeys(hierarchy, realObjectClass, ignoreKeys).map((p) => [p.attr._id, p]))
for (const m of mixins) {
const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys)
for (const key of mkeys) {
keysMap.set(key.attr._id, key)
}
}
const filtredKeys = Array.from(keysMap.values())
const { attributes, collections } = categorizeFields(hierarchy, filtredKeys, collectionArrays, allowedCollections)
const info = await getDocAttrsInfo(mixins, ignoreKeys, realObjectClass, allowedCollections, collectionArrays)
keys = attributes.map((it) => it.key)
const editors: Array<{ key: KeyedAttribute, editor: AnyComponent, category: AttributeCategory }> = []
const newInplaceAttributes: string[] = []
for (const k of collections) {
if (allowedCollections.includes(k.key.key)) continue
const editor = await getFieldEditor(k.key)
if (editor === undefined) continue
if (k.category === 'inplace') {
newInplaceAttributes.push(k.key.key)
}
editors.push({ key: k.key, editor, category: k.category })
}
inplaceAttributes = newInplaceAttributes
fieldEditors = editors.sort((a, b) => AttributeCategoryOrder[a.category] - AttributeCategoryOrder[b.category])
keys = info.keys
inplaceAttributes = info.inplaceAttributes
fieldEditors = info.editors
}
interface MixinEditor {
@ -166,37 +143,18 @@
$: editorFooter = getEditorFooter(_class, object)
$: getEditorOrDefault(realObjectClass, _id)
$: void getEditorOrDefault(realObjectClass, _id)
async function getEditorOrDefault (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<void> {
await updateKeys()
mainEditor = getEditor(_class)
}
async function getFieldEditor (key: KeyedAttribute): Promise<AnyComponent | undefined> {
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
const clazz = hierarchy.getClass(attrClass.attrClass)
const mix = {
array: view.mixin.ArrayEditor,
collection: view.mixin.CollectionEditor,
inplace: view.mixin.InlineAttributEditor,
attribute: view.mixin.AttributeEditor,
object: undefined
}
const mixinRef = mix[attrClass.category]
if (mixinRef) {
const editorMixin = hierarchy.as(clazz, mixinRef)
return (editorMixin as any).editor
} else {
return undefined
}
}
let title: string | undefined = undefined
let rawTitle: string = ''
$: if (object !== undefined) {
getDocLabel(pClient, object).then((t) => {
void getDocLabel(pClient, object).then((t) => {
if (t) {
rawTitle = t
}
@ -216,7 +174,7 @@
let headerLoading = false
$: {
headerLoading = true
getHeaderEditor(realObjectClass).then((r) => {
void getHeaderEditor(realObjectClass).then((r) => {
headerEditor = r
headerLoading = false
})
@ -236,7 +194,7 @@
collectionArrays = ev.detail.collectionArrays ?? []
title = ev.detail.title
mixins = getDocMixins(object, showAllMixins, ignoreMixins, realObjectClass)
updateKeys()
void updateKeys()
}
$: finalTitle = title ?? rawTitle

View File

@ -53,12 +53,14 @@ import { type Restrictions } from '@hcengineering/guest'
import type { Asset, IntlString } from '@hcengineering/platform'
import { getResource, translate } from '@hcengineering/platform'
import {
type AttributeCategory,
AttributeCategoryOrder,
getAttributePresenterClass,
getClient,
hasResource,
isAdminUser,
type AttributeCategory,
type KeyedAttribute
type KeyedAttribute,
getFiltredKeys,
isAdminUser
} from '@hcengineering/presentation'
import {
ErrorPresenter,
@ -1266,3 +1268,67 @@ export const restrictionStore = writable<Restrictions>({
disableNavigation: false,
disableActions: false
})
export async function getDocAttrsInfo (
mixins: Array<Mixin<Doc>>,
ignoreKeys: string[],
_class: Ref<Class<Doc>>,
allowedCollections: string[] = [],
collectionArrays: string[] = []
): Promise<{
keys: KeyedAttribute[]
inplaceAttributes: string[]
editors: Array<{ key: KeyedAttribute, editor: AnyComponent, category: AttributeCategory }>
}> {
const client = getClient()
const hierarchy = client.getHierarchy()
const keysMap = new Map(getFiltredKeys(hierarchy, _class, ignoreKeys).map((p) => [p.attr._id, p]))
for (const m of mixins) {
const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys)
for (const key of mkeys) {
keysMap.set(key.attr._id, key)
}
}
const filteredKeys = Array.from(keysMap.values())
const { attributes, collections } = categorizeFields(hierarchy, filteredKeys, collectionArrays, allowedCollections)
const keys = attributes.map((it) => it.key)
const editors: Array<{ key: KeyedAttribute, editor: AnyComponent, category: AttributeCategory }> = []
const inplaceAttributes: string[] = []
for (const k of collections) {
if (allowedCollections.includes(k.key.key)) continue
const editor = await getAttrEditor(k.key, hierarchy)
if (editor === undefined) continue
if (k.category === 'inplace') {
inplaceAttributes.push(k.key.key)
}
editors.push({ key: k.key, editor, category: k.category })
}
return {
keys,
inplaceAttributes,
editors: editors.sort((a, b) => AttributeCategoryOrder[a.category] - AttributeCategoryOrder[b.category])
}
}
async function getAttrEditor (key: KeyedAttribute, hierarchy: Hierarchy): Promise<AnyComponent | undefined> {
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
const clazz = hierarchy.getClass(attrClass.attrClass)
const mix = {
array: view.mixin.ArrayEditor,
collection: view.mixin.CollectionEditor,
inplace: view.mixin.InlineAttributEditor,
attribute: view.mixin.AttributeEditor,
object: undefined as any
}
const mixinRef = mix[attrClass.category]
if (mixinRef !== undefined) {
const editorMixin = hierarchy.as(clazz, mixinRef)
return (editorMixin as any).editor
} else {
return undefined
}
}