Unify contacts (#778)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-01-11 16:05:53 +07:00 committed by GitHub
parent 987a8d670a
commit d555409ca1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 466 additions and 355 deletions

View File

@ -71,13 +71,14 @@ export class TContact extends TDoc implements Contact {
@Prop(Collection(chunter.class.Comment), 'Comments' as IntlString)
comments?: number
@Prop(TypeString(), 'Location' as IntlString)
city!: string
}
@Model(contact.class.Person, contact.class.Contact)
@UX('Person' as IntlString, contact.icon.Person, undefined, 'name')
export class TPerson extends TContact implements Person {
@Prop(TypeString(), 'City' as IntlString)
city!: string
}
@Model(contact.class.Organization, contact.class.Contact)
@ -85,6 +86,7 @@ export class TPerson extends TContact implements Person {
export class TOrganization extends TContact implements Organization {}
@Model(contact.class.Employee, contact.class.Person)
@UX('Employee' as IntlString, contact.icon.Person)
export class TEmployee extends TPerson implements Employee {}
@Model(contact.class.EmployeeAccount, core.class.Account)
@ -114,44 +116,23 @@ export function createModel (builder: Builder): void {
TEmployeeAccount
)
builder.mixin(contact.class.Persons, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: contact.class.Person,
createItemDialog: contact.component.CreatePerson
}
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectFactory, {
component: contact.component.CreatePerson
})
builder.mixin(contact.class.Organizations, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: contact.class.Organization,
createItemDialog: contact.component.CreateOrganization
}
builder.mixin(contact.class.Organization, core.class.Class, view.mixin.ObjectFactory, {
component: contact.component.CreateOrganization
})
builder.createDoc(workbench.class.Application, core.space.Model, {
label: contact.string.Contacts,
icon: contact.icon.Person,
hidden: false,
navigatorModel: {
spaces: [
{
label: contact.string.Persons,
spaceClass: contact.class.Persons,
addSpaceLabel: contact.string.CreatePersons,
createComponent: contact.component.CreatePersons
},
{
label: contact.string.Organizations,
spaceClass: contact.class.Organizations,
addSpaceLabel: contact.string.CreateOrganizations,
createComponent: contact.component.CreateOrganizations
}
]
}
component: contact.component.Contacts
}, contact.app.Contacts)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: contact.class.Person,
attachTo: contact.class.Contact,
descriptor: view.viewlet.Table,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@ -161,19 +142,11 @@ export function createModel (builder: Builder): void {
'city',
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
'modifiedOn',
{ presenter: contact.component.RolePresenter, label: 'Role' },
'channels'
]
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: contact.class.Organization,
descriptor: view.viewlet.Table,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {},
config: ['', { presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' }, 'modifiedOn', 'channels']
})
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectEditor, {
editor: contact.component.EditPerson
})

View File

@ -36,7 +36,9 @@ export const ids = mergeIds(contactId, contact, {
CreateOrganization: '' as AnyComponent,
CreatePersons: '' as AnyComponent,
CreateOrganizations: '' as AnyComponent,
OrganizationPresenter: '' as AnyComponent
OrganizationPresenter: '' as AnyComponent,
Contacts: '' as AnyComponent,
RolePresenter: '' as AnyComponent
},
string: {
Organizations: '' as IntlString,

View File

@ -100,6 +100,10 @@ export function createModel (builder: Builder): void {
editor: recruit.component.Applications
})
builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, {
component: recruit.component.CreateCandidate
})
builder.createDoc(
workbench.class.Application,
core.space.Model,
@ -235,10 +239,6 @@ export function createModel (builder: Builder): void {
card: recruit.component.KanbanCard
})
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditCandidate
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditApplication
})

View File

@ -44,7 +44,6 @@ export default mergeIds(recruitId, recruit, {
component: {
CreateVacancy: '' as AnyComponent,
CreateApplication: '' as AnyComponent,
EditCandidate: '' as AnyComponent,
KanbanCard: '' as AnyComponent,
ApplicationPresenter: '' as AnyComponent,
ApplicationsPresenter: '' as AnyComponent,
@ -52,7 +51,8 @@ export default mergeIds(recruitId, recruit, {
EditApplication: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
Applications: '' as AnyComponent,
Candidates: '' as AnyComponent
Candidates: '' as AnyComponent,
CreateCandidate: '' as AnyComponent
},
template: {
DefaultVacancy: '' as Ref<KanbanTemplate>

View File

@ -19,7 +19,7 @@ import { Builder, Mixin, Model } from '@anticrm/model'
import core, { TClass, TDoc } from '@anticrm/model-core'
import type { Asset, IntlString, Resource, Status } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import type { Action, ActionTarget, AttributeEditor, AttributePresenter, ObjectEditor, ObjectValidator, Viewlet, ViewletDescriptor } from '@anticrm/view'
import type { Action, ActionTarget, AttributeEditor, AttributePresenter, ObjectEditor, ObjectFactory, ObjectValidator, Viewlet, ViewletDescriptor } from '@anticrm/view'
import view from './plugin'
@Mixin(view.mixin.AttributeEditor, core.class.Class)
@ -42,6 +42,11 @@ export class TObjectValidator extends TClass implements ObjectValidator {
validator!: Resource<(<T extends Doc>(doc: T, client: Client) => Promise<Status<{}>>)>
}
@Mixin(view.mixin.ObjectFactory, core.class.Class)
export class TObjectFactory extends TClass implements ObjectFactory {
component!: AnyComponent
}
@Model(view.class.ViewletDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
component!: AnyComponent
@ -70,7 +75,7 @@ export class TActionTarget extends TDoc implements ActionTarget {
}
export function createModel (builder: Builder): void {
builder.createModel(TAttributeEditor, TAttributePresenter, TObjectEditor, TViewletDescriptor, TViewlet, TAction, TActionTarget, TObjectValidator)
builder.createModel(TAttributeEditor, TAttributePresenter, TObjectEditor, TViewletDescriptor, TViewlet, TAction, TActionTarget, TObjectValidator, TObjectFactory)
builder.mixin(core.class.TypeString, core.class.Class, view.mixin.AttributeEditor, {
editor: view.component.StringEditor

View File

@ -157,7 +157,7 @@ export class Hierarchy {
}
private txMixin (tx: TxMixin<Doc, Doc>): void {
if (tx.objectClass === core.class.Class) {
if (this.isDerived(tx.objectClass, core.class.Class)) {
const obj = this.getClass(tx.objectId as Ref<Class<Obj>>) as any
TxProcessor.updateMixin4Doc(obj, tx.mixin, tx.attributes)
}
@ -304,7 +304,14 @@ export class Hierarchy {
const result = new Map<string, AnyAttribute>()
let ancestors = this.getAncestors(clazz)
if (to !== undefined) {
ancestors = ancestors.filter(c => this.isDerived(c, to) && c !== to)
const toAncestors = this.getAncestors(to)
for (const uto of toAncestors) {
if (ancestors.includes(uto)) {
to = uto
break
}
}
ancestors = ancestors.filter(c => this.isDerived(c, to as Ref<Class<Doc>>) && c !== to)
}
for (const cls of ancestors) {

View File

@ -14,27 +14,22 @@
-->
<script lang="ts">
import type { Asset,IntlString } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte'
import { AnySvelteComponent } from '../types'
import { Action } from '../types'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
export let actions: {
label: IntlString
icon?: Asset | AnySvelteComponent
action: (ctx?: any) => void | Promise<void>
}[] = []
export let actions: Action[] = []
export let ctx: any = undefined
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher()
</script>
<div class="flex-col popup">
{#each actions as action}
<div class="flex-row-center menu-item" on:click={() => {
<div class="flex-row-center menu-item" on:click={() => {
dispatch('close')
action.action(ctx)
action.action(ctx)
}}>
{#if action.icon}
<Icon icon={action.icon} size={'small'} />

View File

@ -13,6 +13,7 @@
"PersonsFolder": "Persons folder",
"AddSocialLinks": "Add social links",
"MakePrivate": "Make private",
"MakePrivateDescription": "Only members can see it"
"MakePrivateDescription": "Only members can see it",
"Create": "Contact"
}
}

View File

@ -39,6 +39,7 @@
"@anticrm/core": "~0.6.11",
"@anticrm/view": "~0.6.0",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/panel": "~0.6.0"
"@anticrm/panel": "~0.6.0",
"@anticrm/view-resources": "~0.6.0"
}
}

View File

@ -0,0 +1,122 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { getClient } from '@anticrm/presentation'
import { Button, EditWithIcon, Icon, IconAdd, IconSearch, Label, ScrollBox, showPopup } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { Table } from '@anticrm/view-resources'
import contact from '../plugin'
import CreateContact from './CreateContact.svelte'
let search = ''
$: resultQuery = search === '' ? { } : { $search: search }
const client = getClient()
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, { attachTo: contact.class.Contact, descriptor: view.viewlet.Table })
function showCreateDialog (ev: Event) {
showPopup(CreateContact, { space: contact.space.Contacts, targetElement: ev.target }, ev.target as HTMLElement)
}
</script>
<div class="contacts-header-container">
<div class="header-container">
<div class="flex-row-center">
<span class="icon"><Icon icon={contact.icon.Person} size={'small'}/></span>
<span class="label"><Label label={contact.string.Contacts}/></span>
</div>
</div>
<EditWithIcon icon={IconSearch} placeholder={'Search'} bind:value={search} on:change={() => { resultQuery = {} } } />
<Button icon={IconAdd} label={contact.string.Create} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)}/>
</div>
<div class="container">
<div class="panel-component">
<ScrollBox vertical stretch noShift>
{#await tableDescriptor then descr}
{#if descr}
<Table
_class={contact.class.Contact}
config={descr.config}
options={descr.options}
query={ resultQuery }
enableChecking
/>
{/if}
{/await}
</ScrollBox>
</div>
</div>
<style lang="scss">
.container {
display: flex;
height: 100%;
padding-bottom: 1.25rem;
margin-top: 2rem;
.panel-component {
flex-grow: 1;
display: flex;
flex-direction: column;
margin-right: 1rem;
height: 100%;
border-radius: 1.25rem;
background-color: var(--theme-bg-color);
overflow: hidden;
}
}
.contacts-header-container {
display: grid;
grid-template-columns: auto;
grid-auto-flow: column;
grid-auto-columns: min-content;
gap: .75rem;
align-items: center;
padding: 0 1.75rem 0 2.5rem;
height: 4rem;
min-height: 4rem;
.header-container {
display: flex;
flex-direction: column;
flex-grow: 1;
.icon {
margin-right: .5rem;
opacity: .6;
}
.label, .description {
flex-grow: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 35rem;
}
.label {
font-weight: 500;
font-size: 1rem;
color: var(--theme-caption-color);
}
.description {
font-size: .75rem;
color: var(--theme-content-trans-color);
}
}
}
</style>

View File

@ -0,0 +1,32 @@
<script lang='ts'>
import { Asset } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import { Menu, Action, showPopup, closePopup } from '@anticrm/ui'
import view from '@anticrm/view'
import contact from '../plugin'
export let targetElement: HTMLElement
const client = getClient()
const actions: Action[] = []
const hierarchy = client.getHierarchy()
client.getHierarchy().getDescendants(contact.class.Contact).forEach((v) => {
const cl = hierarchy.getClass(v)
if (hierarchy.hasMixin(cl, view.mixin.ObjectFactory)) {
const f = hierarchy.as(cl, view.mixin.ObjectFactory)
actions.push({
icon: cl.icon as Asset,
label: cl.label,
action: async () => {
closePopup()
showPopup(f.component, {}, targetElement)
}
})
}
})
</script>
<Menu actions={actions}/>

View File

@ -26,10 +26,6 @@
import contact from '../plugin'
import Company from './icons/Company.svelte'
export let space: Ref<Space>
let _space = space
export function canClose (): boolean {
return object.name === ''
}
@ -42,7 +38,7 @@
const client = getClient()
async function createOrganization () {
await client.createDoc(contact.class.Organization, _space, object)
await client.createDoc(contact.class.Organization, contact.space.Contacts, object)
dispatch('close')
}
@ -52,10 +48,7 @@
label={'Create organization'}
okAction={createOrganization}
canSave={object.name.length > 0}
spaceClass={contact.class.Organizations}
spaceLabel={contact.string.OrganizationsFolder}
spacePlaceholder={contact.string.SelectFolder}
bind:space={_space}
space={contact.space.Contacts}
on:close={() => {
dispatch('close')
}}

View File

@ -25,10 +25,6 @@
import { combineName, Person } from '@anticrm/contact'
import contact from '../plugin'
export let space: Ref<Space>
let _space = space
let firstName = ''
let lastName = ''
@ -48,7 +44,7 @@
channels: object.channels
}
await client.createDoc(contact.class.Person, _space, person)
await client.createDoc(contact.class.Person, contact.space.Contacts, person)
dispatch('close')
}
@ -58,10 +54,7 @@
label={contact.string.CreatePerson}
okAction={createPerson}
canSave={firstName.length > 0 && lastName.length > 0}
spaceClass={contact.class.Persons}
spaceLabel={contact.string.PersonsFolder}
spacePlaceholder={contact.string.SelectFolder}
bind:space={_space}
bind:space={contact.space.Contacts}
on:close={() => {
dispatch('close')
}}

View File

@ -25,10 +25,11 @@
getClient,
KeyedAttribute
} from '@anticrm/presentation'
import { AnyComponent, Component, getPlatformColorForText, Label } from '@anticrm/ui'
import { AnyComponent, Component, Label } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher, onDestroy } from 'svelte'
import contact from '../plugin'
import { getMixinStyle } from '../utils'
export let _id: Ref<Contact>
let object: Contact
@ -77,8 +78,8 @@
return keys
}
function getFiltredKeys (objectClass: Ref<Class<Doc>>, ignoreKeys: string[]): KeyedAttribute[] {
const keys = [...hierarchy.getAllAttributes(objectClass).entries()]
function getFiltredKeys (objectClass: Ref<Class<Doc>>, ignoreKeys: string[], to?: Ref<Class<Doc>>): KeyedAttribute[] {
const keys = [...hierarchy.getAllAttributes(objectClass, to).entries()]
.filter(([, value]) => value.hidden !== true)
.map(([key, attr]) => ({ key, attr }))
@ -86,7 +87,7 @@
}
function updateKeys (ignoreKeys: string[]): void {
const filtredKeys = getFiltredKeys(selectedClass ?? object._class, ignoreKeys)
const filtredKeys = getFiltredKeys(selectedClass ?? object._class, ignoreKeys, selectedClass !== objectClass._id ? objectClass._id : undefined)
keys = collectionsFilter(filtredKeys, false)
collectionKeys = collectionsFilter(filtredKeys, true)
}
@ -130,14 +131,6 @@
$: icon = (objectClass?.icon ?? contact.class.Person) as Asset
function getStyle (id: Ref<Class<Doc>>, selected: boolean): string {
const color = getPlatformColorForText(id as string)
return `
background: ${color + (selected ? 'ff' : '33')};
border: 1px solid ${color + (selected ? '0f' : '66')};
`
}
let mainEditor: HTMLElement
let prevEditor: HTMLElement
let maxHeight = 0
@ -193,13 +186,13 @@
{#if mixins.length > 0}
<div class="mixin-container">
<div class="mixin-selector"
style={getStyle(objectClass._id, selectedClass === objectClass._id)}
style={getMixinStyle(objectClass._id, selectedClass === objectClass._id)}
on:click={() => { selectedClass = objectClass._id; selectedMixin = undefined }}>
<Label label={objectClass.label} />
</div>
{#each mixins as mixin}
<div class="mixin-selector"
style={getStyle(mixin._id, selectedClass === mixin._id)}
style={getMixinStyle(mixin._id, selectedClass === mixin._id)}
on:click={() => { selectedClass = mixin._id; selectedMixin = mixin }}>
<Label label={mixin.label} />
</div>

View File

@ -17,11 +17,12 @@
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
import { getCurrentAccount, Ref, Space } from '@anticrm/core'
import { CircleButton, EditBox, showPopup, IconEdit, IconAdd, Label, IconActivity } from '@anticrm/ui'
import { getClient, createQuery, Channels, Avatar } from '@anticrm/presentation'
import { getClient, createQuery, Channels, Avatar, AttributeEditor } from '@anticrm/presentation'
import setting from '@anticrm/setting'
import { IntegrationType } from '@anticrm/setting'
import contact from '../plugin'
import { combineName, getFirstName, getLastName, Person } from '@anticrm/contact'
import Edit from './icons/Edit.svelte'
export let object: Person
@ -58,7 +59,7 @@
integrations = new Set(res.map((p) => p.type))
})
const sendOpen = () => dispatch('open', { ignoreKeys: ['comments', 'name', 'channels'] })
const sendOpen = () => dispatch('open', { ignoreKeys: ['comments', 'name', 'channels', 'city'] })
onMount(sendOpen)
afterUpdate(sendOpen)
</script>
@ -76,6 +77,9 @@
<div class="name">
<EditBox placeholder="Appleseed" maxWidth="20rem" bind:value={lastName} on:change={lastNameChange} />
</div>
<div class="location">
<AttributeEditor maxWidth="20rem" _class={contact.class.Person} {object} key="city" />
</div>
</div>
<div class="separator" />
@ -97,7 +101,7 @@
<Channels value={object.channels} size={'small'} {integrations} on:click />
<div class="ml-1">
<CircleButton
icon={IconEdit}
icon={Edit}
size={'small'}
selected
on:click={(ev) =>
@ -132,6 +136,10 @@
margin-left: 0.5rem;
}
}
.location {
margin-top: 0.25rem;
font-size: 0.75rem;
}
.separator {
margin: 1rem 0;

View File

@ -0,0 +1,72 @@
<!--
//
// Copyright © 2021 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 { Contact } from '@anticrm/contact'
import { ClassifierKind, Doc, Mixin } from '@anticrm/core'
import {
getClient
} from '@anticrm/presentation'
import { Label } from '@anticrm/ui'
import contact from '../plugin'
import { getMixinStyle } from '../utils'
export let value: Contact
const client = getClient()
const hierarchy = client.getHierarchy()
let mixins: Mixin<Doc>[] = []
$: if (value !== undefined) {
mixins = hierarchy
.getDescendants(contact.class.Contact)
.filter((m) => hierarchy.getClass(m).kind === ClassifierKind.MIXIN && hierarchy.hasMixin(value, m))
.map((m) => hierarchy.getClass(m) as Mixin<Doc>)
}
</script>
{#if mixins.length > 0}
<div class="mixin-container">
{#each mixins as mixin}
<div class="mixin-selector"
style={getMixinStyle(mixin._id, true)}>
<Label label={mixin.label} />
</div>
{/each}
</div>
{/if}
<style lang="scss">
.mixin-container {
display: flex;
.mixin-selector {
margin-left: 8px;
cursor: pointer;
height: 24px;
min-width: 84px;
border-radius: 8px;
font-weight: 500;
font-size: 10px;
text-transform: uppercase;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@ -0,0 +1,24 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'var(--theme-caption-color)'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3,6.5l1.9-1.9c1.1-1.1,1.1-2.8,0-3.8c-1.1-1.1-2.8-1.1-3.8,0L9.5,2.7C10.4,4.3,11.7,5.6,13.3,6.5z M12.3,7.6c-1.6-1-2.9-2.3-3.8-3.8l-6.3,6.3l0,0C1.5,10.7,1.2,11,1,11.4c-0.2,0.4-0.3,0.8-0.5,1.6l-0.4,2c-0.1,0.5-0.1,0.7,0,0.9 c0.1,0.1,0.4,0.1,0.9,0l2-0.4c0.8-0.2,1.3-0.3,1.6-0.5c0.4-0.2,0.7-0.5,1.3-1.1L12.3,7.6z"/>
</svg>

View File

@ -26,7 +26,9 @@ import EditOrganization from './components/EditOrganization.svelte'
import CreatePersons from './components/CreatePersons.svelte'
import CreateOrganizations from './components/CreateOrganizations.svelte'
import SocialEditor from './components/SocialEditor.svelte'
import Contacts from './components/Contacts.svelte'
import { Resources } from '@anticrm/platform'
import RolePresenter from './components/RolePresenter.svelte'
export { ContactPresenter, EditContact }
@ -42,6 +44,8 @@ export default async (): Promise<Resources> => ({
EditOrganization,
CreatePersons,
CreateOrganizations,
SocialEditor
SocialEditor,
Contacts,
RolePresenter
}
})

View File

@ -32,6 +32,7 @@ export default mergeIds(contactId, contact, {
AddSocialLinks: '' as IntlString,
Name: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString
MakePrivateDescription: '' as IntlString,
Create: '' as IntlString
}
})

View File

@ -0,0 +1,10 @@
import { Class, Doc, Ref } from '@anticrm/core'
import { getPlatformColorForText } from '@anticrm/ui'
export function getMixinStyle (id: Ref<Class<Doc>>, selected: boolean): string {
const color = getPlatformColorForText(id as string)
return `
background: ${color + (selected ? 'ff' : '33')};
border: 1px solid ${color + (selected ? '0f' : '66')};
`
}

View File

@ -54,19 +54,21 @@ export interface Contact extends Doc {
attachments?: number
comments?: number
channels: Channel[]
}
/**
* @public
*/
export interface Person extends Contact {
city: string
}
/**
* @public
*/
export interface Organization extends Contact {}
export interface Person extends Contact {
}
/**
* @public
*/
export interface Organization extends Contact {
}
/**
* @public
@ -155,6 +157,7 @@ export default plugin(contactId, {
Company: '' as Asset
},
space: {
Employee: '' as Ref<Space>
Employee: '' as Ref<Space>,
Contacts: '' as Ref<Space>
}
})

View File

@ -37,6 +37,7 @@ import { toIntl } from '..'
<tr>
<th>ID</th>
<th>Class</th>
<th>ObjectID</th>
<th>Body</th>
</tr>
</thead>
@ -44,6 +45,7 @@ import { toIntl } from '..'
{#each txes as tx}
<tr class='tr-body'>
<td>{tx.index}</td>
<td>{tx._class}</td>
<td>{tx.objectId}</td>
<td>{tx.objectClass}</td>
<td>

View File

@ -16,7 +16,7 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import contact, { combineName, Person } from '@anticrm/contact'
import type { Data, MixinData, Ref, Space } from '@anticrm/core'
import type { Data, MixinData, Ref } from '@anticrm/core'
import { generateId } from '@anticrm/core'
import { setPlatformStatus, unknownError } from '@anticrm/platform'
import { Avatar, Card, Channels, getClient, PDFViewer } from '@anticrm/presentation'
@ -29,10 +29,6 @@
import FileUpload from './icons/FileUpload.svelte'
import YesNo from './YesNo.svelte'
export let space: Ref<Space>
let _space = space
let firstName = ''
let lastName = ''
@ -66,13 +62,13 @@
remote: object.remote
}
const id = await client.createDoc(contact.class.Person, _space, candidate, candidateId)
await client.createMixin(id as Ref<Person>, contact.class.Person, _space, recruit.mixin.Candidate, candidateData)
const id = await client.createDoc(contact.class.Person, contact.space.Contacts, candidate, candidateId)
await client.createMixin(id as Ref<Person>, contact.class.Person, contact.space.Contacts, recruit.mixin.Candidate, candidateData)
console.log('resume name', resume.name)
if (resume.uuid !== undefined) {
client.addCollection(attachment.class.Attachment, space, id, contact.class.Person, 'attachments', {
client.addCollection(attachment.class.Attachment, contact.space.Contacts, id, contact.class.Person, 'attachments', {
name: resume.name,
file: resume.uuid,
size: resume.size,
@ -123,10 +119,7 @@
<Card label={'Create Candidate'}
okAction={createCandidate}
canSave={firstName.length > 0 && lastName.length > 0}
spaceClass={recruit.class.Candidates}
spaceLabel={'Talent Pool'}
spacePlaceholder={'Select pool'}
bind:space={_space}
space={contact.space.Contacts}
on:close={() => { dispatch('close') }}>
<!-- <StatusComponent slot="error" status={{ severity: Severity.ERROR, code: 'Cant save the object because it already exists' }} /> -->

View File

@ -1,148 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { afterUpdate, createEventDispatcher, onMount } from 'svelte'
import { getCurrentAccount, Ref, Space } from '@anticrm/core'
import { CircleButton, EditBox, showPopup, IconAdd, Label, IconActivity } from '@anticrm/ui'
import { getClient, createQuery, Channels, AttributeEditor, Avatar } from '@anticrm/presentation'
import type { Candidate } from '@anticrm/recruit'
import Edit from './icons/Edit.svelte'
import recruit from '../plugin'
import setting from '@anticrm/setting'
import { IntegrationType } from '@anticrm/setting'
import contact, { combineName, getFirstName, getLastName } from '@anticrm/contact'
export let object: Candidate
let firstName = getFirstName(object.name)
let lastName = getLastName(object.name)
const client = getClient()
const dispatch = createEventDispatcher()
function saveChannels (result: any) {
if (result !== undefined) {
object.channels = result
client.updateDoc(object._class, object.space, object._id, { channels: result })
}
}
function firstNameChange () {
client.updateDoc(object._class, object.space, object._id, {
name: combineName(firstName, getLastName(object.name))
})
}
function lastNameChange () {
client.updateDoc(object._class, object.space, object._id, {
name: combineName(getFirstName(object.name), lastName)
})
}
const accountId = getCurrentAccount()._id
let integrations: Set<Ref<IntegrationType>> = new Set<Ref<IntegrationType>>()
const settingsQuery = createQuery()
$: settingsQuery.query(setting.class.Integration, { space: accountId as string as Ref<Space> }, (res) => {
integrations = new Set(res.map((p) => p.type))
})
const sendOpen = () => dispatch('open', { ignoreKeys: ['comments', 'name', 'channels', 'title'] })
onMount(sendOpen)
afterUpdate(sendOpen)
</script>
{#if object !== undefined}
<div class="flex-row-streach flex-grow">
<div class="mr-8">
<Avatar avatar={object.avatar} size={'x-large'} />
</div>
<div class="flex-grow flex-col">
<div class="flex-grow flex-col">
<div class="name">
<EditBox placeholder="John" maxWidth="20rem" bind:value={firstName} on:change={firstNameChange} />
</div>
<div class="name">
<EditBox placeholder="Appleseed" maxWidth="20rem" bind:value={lastName} on:change={lastNameChange} />
</div>
<div class="title">
<AttributeEditor maxWidth="20rem" _class={recruit.mixin.Candidate} {object} key="title" />
</div>
</div>
<div class="separator" />
<div class="flex-between channels">
<div class="flex-row-center">
{#if !object.channels || object.channels.length === 0}
<CircleButton
icon={IconAdd}
size={'small'}
selected
on:click={(ev) =>
showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
saveChannels(result)
})}
/>
<span class="ml-2"><Label label={'Add social links'} /></span>
{:else}
<Channels value={object.channels} {integrations} size={'small'} on:click />
<div class="ml-1">
<CircleButton
icon={Edit}
size={'small'}
on:click={(ev) =>
showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
saveChannels(result)
})}
/>
</div>
{/if}
</div>
<div class="flex-row-center">
<a href={'#'} class="flex-row-center" on:click>
<CircleButton icon={IconActivity} size={'small'} primary on:click />
<span class="ml-2 small-text">View activity</span>
</a>
</div>
</div>
</div>
</div>
{/if}
<style lang="scss">
.name {
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
.title {
margin-top: 0.25rem;
font-size: 0.75rem;
}
.channels {
margin-top: 0.75rem;
span {
margin-left: 0.5rem;
}
}
.separator {
margin: 1rem 0;
height: 1px;
background-color: var(--theme-card-divider);
}
</style>

View File

@ -15,12 +15,10 @@
<script lang="ts">
import { Avatar } from '@anticrm/presentation'
import { showPopup, Label, ActionIcon, IconMoreH } from '@anticrm/ui'
import { showPopup, ActionIcon, IconMoreH } from '@anticrm/ui'
import type { WithLookup } from '@anticrm/core'
import type { Applicant } from '@anticrm/recruit'
import EditCandidate from './EditCandidate.svelte'
import { CommentsPresenter } from '@anticrm/chunter-resources'
import { AttachmentsPresenter } from '@anticrm/attachment-resources'
import { formatName } from '@anticrm/contact'
@ -30,7 +28,7 @@
export let object: WithLookup<Applicant>
export let draggable: boolean
function showCandidate() {
function showCandidate () {
showPopup(EditContact, { _id: object.attachedTo }, 'full')
}
</script>

View File

@ -14,22 +14,20 @@
//
import type { Client, Doc } from '@anticrm/core'
import CreateVacancy from './components/CreateVacancy.svelte'
import CreateApplication from './components/CreateApplication.svelte'
import EditCandidate from './components/EditCandidate.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import EditVacancy from './components/EditVacancy.svelte'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import Applications from './components/Applications.svelte'
import EditApplication from './components/EditApplication.svelte'
import Candidates from './components/Candidates.svelte'
import { showPopup } from '@anticrm/ui'
import { OK, Resources, Severity, Status } from '@anticrm/platform'
import { Applicant } from '@anticrm/recruit'
import { showPopup } from '@anticrm/ui'
import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import Applications from './components/Applications.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
import Candidates from './components/Candidates.svelte'
import CreateApplication from './components/CreateApplication.svelte'
import CreateCandidate from './components/CreateCandidate.svelte'
import CreateVacancy from './components/CreateVacancy.svelte'
import EditApplication from './components/EditApplication.svelte'
import EditVacancy from './components/EditVacancy.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import recruit from './plugin'
async function createApplication (object: Doc): Promise<void> {
@ -63,7 +61,6 @@ export default async (): Promise<Resources> => ({
component: {
CreateVacancy,
CreateApplication,
EditCandidate,
EditApplication,
KanbanCard,
ApplicationPresenter,
@ -71,6 +68,7 @@ export default async (): Promise<Resources> => ({
EditVacancy,
TemplatesIcon,
Applications,
Candidates
Candidates,
CreateCandidate
}
})

View File

@ -14,20 +14,16 @@
-->
<script lang="ts">
import type { Doc, Class, Ref } from '@anticrm/core'
import type { Asset, IntlString, Resource } from '@anticrm/platform'
import type { Class, Doc, Ref } from '@anticrm/core'
import type { Asset, Resource } from '@anticrm/platform'
import { getResource } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import { Menu } from '@anticrm/ui'
import { Action, Menu } from '@anticrm/ui'
import { getActions } from '../utils'
export let object: Doc
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let actions: {
label: IntlString
icon: Asset
action: () => void
}[] = []
export let actions: Action[] = []
const client = getClient()
@ -39,8 +35,8 @@
getActions(client, object, baseMenuClass).then(result => {
actions = result.map(a => ({
label: a.label,
icon: a.icon,
action: () => { invokeAction(a.action) }
icon: a.icon as Asset,
action: async () => { invokeAction(a.action) }
}))
})

View File

@ -121,6 +121,16 @@ export interface BuildModelOptions {
ignoreMissing?: boolean
}
/**
* Define document create popup widget
*
* @public
*
*/
export interface ObjectFactory extends Class<Obj> {
component: AnyComponent
}
/**
* @public
*/
@ -129,7 +139,8 @@ const view = plugin(viewId, {
AttributeEditor: '' as Ref<Mixin<AttributeEditor>>,
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>,
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>
},
class: {
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,

View File

@ -12,28 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import ActivityStatus from './ActivityStatus.svelte'
import Applications from './Applications.svelte'
import NavHeader from './NavHeader.svelte'
import { onDestroy } from 'svelte'
import type { Ref, Space, Client } from '@anticrm/core'
import type { Client, Ref, Space } from '@anticrm/core'
import core from '@anticrm/core'
import { Avatar, createQuery, setClient } from '@anticrm/presentation'
import {
AnyComponent,
AnySvelteComponent,
closeTooltip,
Component,
location,
Popup,
showPopup,
TooltipInstance
} from '@anticrm/ui'
import type { Application, NavigatorModel, ViewConfiguration } from '@anticrm/workbench'
import { setClient, Avatar, createQuery } from '@anticrm/presentation'
import { onDestroy } from 'svelte'
import workbench from '../plugin'
import AccountPopup from './AccountPopup.svelte'
import ActivityStatus from './ActivityStatus.svelte'
import AppItem from './AppItem.svelte'
import Applications from './Applications.svelte'
import Archive from './Archive.svelte'
import TopMenu from './icons/TopMenu.svelte'
import NavHeader from './NavHeader.svelte'
import Navigator from './Navigator.svelte'
import SpaceView from './SpaceView.svelte'
import { AnyComponent, Component, location, Popup, showPopup, TooltipInstance, closeTooltip, ActionIcon, IconEdit, AnySvelteComponent } from '@anticrm/ui'
import core from '@anticrm/core'
import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte'
import TopMenu from './icons/TopMenu.svelte'
import Archive from './Archive.svelte'
export let client: Client
@ -48,35 +52,37 @@
let createItemDialog: AnyComponent | undefined
let navigatorModel: NavigatorModel | undefined
onDestroy(location.subscribe(async (loc) => {
currentApp = loc.path[1] as Ref<Application>
currentApplication = (await client.findAll(workbench.class.Application, { _id: currentApp }))[0]
navigatorModel = currentApplication?.navigatorModel
let currentFolder = loc.path[2] as Ref<Space>
ownSpecialComponent = getOwnSpecialComponent(currentFolder)
onDestroy(
location.subscribe(async (loc) => {
currentApp = loc.path[1] as Ref<Application>
currentApplication = (await client.findAll(workbench.class.Application, { _id: currentApp }))[0]
navigatorModel = currentApplication?.navigatorModel
const currentFolder = loc.path[2] as Ref<Space>
ownSpecialComponent = getOwnSpecialComponent(currentFolder)
if (ownSpecialComponent !== undefined) {
return
}
specialComponent = getSpecialComponent(currentFolder)
if (ownSpecialComponent !== undefined) {
return
}
if (specialComponent !== undefined) {
return
}
specialComponent = getSpecialComponent(currentFolder)
const space = (await client.findAll(core.class.Space, { _id: currentFolder }))[0]
currentSpace = currentFolder
if (space) {
const spaceClass = client.getHierarchy().getClass(space._class) // (await client.findAll(core.class.Class, { _id: space._class }))[0]
const view = client.getHierarchy().as(spaceClass, workbench.mixin.SpaceView)
currentView = view.view
createItemDialog = currentView.createItemDialog
} else {
currentView = undefined
createItemDialog = undefined
}
}))
if (specialComponent !== undefined) {
return
}
const space = (await client.findAll(core.class.Space, { _id: currentFolder }))[0]
currentSpace = currentFolder
if (space) {
const spaceClass = client.getHierarchy().getClass(space._class) // (await client.findAll(core.class.Class, { _id: space._class }))[0]
const view = client.getHierarchy().as(spaceClass, workbench.mixin.SpaceView)
currentView = view.view
createItemDialog = currentView.createItemDialog
} else {
currentView = undefined
createItemDialog = undefined
}
})
)
function getOwnSpecialComponent (id: string): AnySvelteComponent | undefined {
if (id === 'archive') {
@ -85,14 +91,16 @@
}
function getSpecialComponent (id: string): AnyComponent | undefined {
let special = navigatorModel?.specials?.find((x) => x.id === id)
const special = navigatorModel?.specials?.find((x) => x.id === id)
return special?.component
}
let apps: Application[] = []
const query = createQuery()
$: query.query(workbench.class.Application, { hidden: false }, result => { apps = result })
$: query.query(workbench.class.Application, { hidden: false }, (result) => {
apps = result
})
let visibileNav: boolean = true
const toggleNav = async () => {
@ -104,16 +112,18 @@
{#if client}
<svg class="svg-mask">
<clipPath id="notify-normal">
<path d="M0,0v52.5h52.5V0H0z M34,23.2c-3.2,0-5.8-2.6-5.8-5.8c0-3.2,2.6-5.8,5.8-5.8c3.2,0,5.8,2.6,5.8,5.8 C39.8,20.7,37.2,23.2,34,23.2z"/>
<path
d="M0,0v52.5h52.5V0H0z M34,23.2c-3.2,0-5.8-2.6-5.8-5.8c0-3.2,2.6-5.8,5.8-5.8c3.2,0,5.8,2.6,5.8,5.8 C39.8,20.7,37.2,23.2,34,23.2z"
/>
</clipPath>
<clipPath id="notify-small">
<path d="M0,0v45h45V0H0z M29.5,20c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5S32.3,20,29.5,20z"/>
<path d="M0,0v45h45V0H0z M29.5,20c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5S32.3,20,29.5,20z" />
</clipPath>
</svg>
<div class="container">
<div class="panel-app" on:click={toggleNav}>
<div class="flex-col">
<ActivityStatus status="active"/>
<ActivityStatus status="active" />
<AppItem
icon={TopMenu}
label={visibileNav ? workbench.string.HideMenu : workbench.string.ShowMenu}
@ -121,28 +131,37 @@
action={toggleNav}
/>
</div>
<Applications {apps} active={currentApp}/>
<Applications {apps} active={currentApp} />
<div class="flex-center" style="min-height: 6.25rem;">
<div class="cursor-pointer" on:click|stopPropagation={(el) => { showPopup(AccountPopup, { }, 'account') }}>
<div
class="cursor-pointer"
on:click|stopPropagation={(el) => {
showPopup(AccountPopup, {}, 'account')
}}
>
<Avatar size={'medium'} />
</div>
</div>
</div>
{#if navigator && visibileNav}
<div class="panel-navigator">
{#if currentApplication}
<NavHeader label={currentApplication.label} />
{/if}
<Navigator model={navigatorModel} />
</div>
{#if currentApplication && navigatorModel && navigator && visibileNav}
<div class="panel-navigator">
{#if currentApplication}
<NavHeader label={currentApplication.label} />
{/if}
<Navigator model={navigatorModel} />
</div>
{/if}
<div class="panel-component">
{#if currentApplication && currentApplication.component}
<Component is={currentApplication.component} />
{/if}
{#if ownSpecialComponent}
<svelte:component this={ownSpecialComponent} model={navigatorModel} />
{:else if specialComponent}
<Component is={specialComponent} />
{:else}
<SpaceView {currentSpace} {currentView} {createItemDialog}/>
<SpaceView {currentSpace} {currentView} {createItemDialog} />
{/if}
</div>
<!-- <div class="aside"><Chat thread/></div> -->

View File

@ -28,6 +28,9 @@ export interface Application extends Doc {
icon: Asset
hidden: boolean
navigatorModel?: NavigatorModel
// Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model
component?: AnyComponent
}
/**