mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-16 05:13:06 +00:00
Support Contacts (#2015)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
922c06d5f1
commit
3a5c431521
@ -19,13 +19,14 @@ import type {
|
||||
Contact,
|
||||
Employee,
|
||||
EmployeeAccount,
|
||||
Member,
|
||||
Organization,
|
||||
Organizations,
|
||||
Person,
|
||||
Persons,
|
||||
Status
|
||||
} from '@anticrm/contact'
|
||||
import type { Class, Domain, Ref, Timestamp } from '@anticrm/core'
|
||||
import type { Class, Domain, FindOptions, Lookup, Ref, Timestamp } from '@anticrm/core'
|
||||
import { DOMAIN_MODEL, IndexKind } from '@anticrm/core'
|
||||
import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
@ -35,8 +36,8 @@ import presentation from '@anticrm/model-presentation'
|
||||
import view, { actionTemplates, createAction } from '@anticrm/model-view'
|
||||
import workbench from '@anticrm/model-workbench'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import contact from './plugin'
|
||||
import setting from '@anticrm/setting'
|
||||
import contact from './plugin'
|
||||
|
||||
export const DOMAIN_CONTACT = 'contact' as Domain
|
||||
export const DOMAIN_CHANNEL = 'channel' as Domain
|
||||
@ -94,9 +95,19 @@ export class TChannel extends TAttachedDoc implements Channel {
|
||||
@UX(contact.string.Person, contact.icon.Person, undefined, 'name')
|
||||
export class TPerson extends TContact implements Person {}
|
||||
|
||||
@Model(contact.class.Member, core.class.AttachedDoc, DOMAIN_CONTACT)
|
||||
@UX(contact.string.Member, contact.icon.Person, undefined, 'name')
|
||||
export class TMember extends TAttachedDoc implements Member {
|
||||
@Prop(TypeRef(contact.class.Contact), contact.string.Contact)
|
||||
contact!: Ref<Contact>
|
||||
}
|
||||
|
||||
@Model(contact.class.Organization, contact.class.Contact)
|
||||
@UX(contact.string.Organization, contact.icon.Company, undefined, 'name')
|
||||
export class TOrganization extends TContact implements Organization {}
|
||||
export class TOrganization extends TContact implements Organization {
|
||||
@Prop(Collection(contact.class.Member), contact.string.Members)
|
||||
members!: number
|
||||
}
|
||||
|
||||
@Model(contact.class.Status, core.class.AttachedDoc, DOMAIN_CONTACT)
|
||||
@UX(contact.string.Status)
|
||||
@ -139,7 +150,8 @@ export function createModel (builder: Builder): void {
|
||||
TEmployee,
|
||||
TEmployeeAccount,
|
||||
TChannel,
|
||||
TStatus
|
||||
TStatus,
|
||||
TMember
|
||||
)
|
||||
|
||||
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectFactory, {
|
||||
@ -162,6 +174,33 @@ export function createModel (builder: Builder): void {
|
||||
contact.app.Contacts
|
||||
)
|
||||
|
||||
const contactLookup: Lookup<Contact> = {
|
||||
_id: {
|
||||
channels: contact.class.Channel
|
||||
}
|
||||
}
|
||||
const memberOptions: FindOptions<Member> = {
|
||||
lookup: {
|
||||
contact: [contact.class.Contact, contactLookup]
|
||||
}
|
||||
}
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: contact.class.Member,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: ['', '$lookup.contact.$lookup.channels', 'modifiedOn'],
|
||||
options: memberOptions,
|
||||
hiddenKeys: ['name']
|
||||
},
|
||||
contact.viewlet.TableMember
|
||||
)
|
||||
|
||||
builder.mixin(contact.class.Member, core.class.Class, view.mixin.ObjectEditor, {
|
||||
editor: contact.component.EditMember
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: contact.class.Contact,
|
||||
descriptor: view.viewlet.Table,
|
||||
@ -193,6 +232,13 @@ export function createModel (builder: Builder): void {
|
||||
editor: contact.component.OrganizationEditor
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Member, core.class.Class, view.mixin.CollectionEditor, {
|
||||
editor: contact.component.Members
|
||||
})
|
||||
builder.mixin(contact.class.Member, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: contact.component.MemberPresenter
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.Person, core.class.Class, view.mixin.AttributeEditor, {
|
||||
editor: contact.component.PersonEditor
|
||||
})
|
||||
@ -305,6 +351,8 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
builder.mixin(contact.class.Contact, core.class.Class, setting.mixin.Editable, {})
|
||||
|
||||
builder.mixin(contact.class.Member, core.class.Class, setting.mixin.Editable, {})
|
||||
|
||||
builder.createDoc(
|
||||
presentation.class.ObjectSearchCategory,
|
||||
core.space.Model,
|
||||
|
@ -13,20 +13,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { IntlString, mergeIds, Resource } from '@anticrm/platform'
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import { contactId } from '@anticrm/contact'
|
||||
import contact from '@anticrm/contact-resources/src/plugin'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import {} from '@anticrm/core'
|
||||
import { ObjectSearchCategory, ObjectSearchFactory } from '@anticrm/model-presentation'
|
||||
import { IntlString, mergeIds, Resource } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
export default mergeIds(contactId, contact, {
|
||||
component: {
|
||||
PersonPresenter: '' as AnyComponent,
|
||||
ContactPresenter: '' as AnyComponent,
|
||||
ChannelsPresenter: '' as AnyComponent,
|
||||
CreatePerson: '' as AnyComponent,
|
||||
EditPerson: '' as AnyComponent,
|
||||
EditOrganization: '' as AnyComponent,
|
||||
CreatePersons: '' as AnyComponent,
|
||||
@ -36,7 +35,10 @@ export default mergeIds(contactId, contact, {
|
||||
EmployeeAccountPresenter: '' as AnyComponent,
|
||||
OrganizationEditor: '' as AnyComponent,
|
||||
EmployeePresenter: '' as AnyComponent,
|
||||
PersonEditor: '' as AnyComponent
|
||||
PersonEditor: '' as AnyComponent,
|
||||
Members: '' as AnyComponent,
|
||||
MemberPresenter: '' as AnyComponent,
|
||||
EditMember: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Persons: '' as IntlString,
|
||||
|
@ -54,7 +54,7 @@ import preference, { TPreference } from '@anticrm/model-preference'
|
||||
import view from './plugin'
|
||||
|
||||
export { viewOperation } from './migration'
|
||||
export { ViewAction }
|
||||
export { ViewAction, Viewlet }
|
||||
|
||||
export function createAction<T extends Doc = Doc, P = Record<string, any>> (
|
||||
builder: Builder,
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
export let size: IconSize
|
||||
|
||||
const fill: string = 'var(--theme-caption-color)'
|
||||
export let fill: string = 'var(--theme-caption-color)'
|
||||
</script>
|
||||
|
||||
<svg class="svg-avatar {size}" {fill} viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||
|
@ -16,7 +16,8 @@
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { onMount } from 'svelte'
|
||||
import { registerFocus } from '../focus'
|
||||
import type { AnySvelteComponent, ButtonKind, ButtonShape, ButtonSize } from '../types'
|
||||
import { tooltip } from '../tooltips'
|
||||
import type { AnySvelteComponent, ButtonKind, ButtonShape, ButtonSize, LabelAndProps } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Spinner from './Spinner.svelte'
|
||||
@ -41,6 +42,8 @@
|
||||
export let id: string | undefined = undefined
|
||||
export let input: HTMLButtonElement | undefined = undefined
|
||||
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
|
||||
$: iconOnly = label === undefined && $$slots.content === undefined
|
||||
|
||||
onMount(() => {
|
||||
@ -80,6 +83,7 @@
|
||||
|
||||
<!-- {focusIndex} -->
|
||||
<button
|
||||
use:tooltip={showTooltip}
|
||||
bind:this={input}
|
||||
class="button {kind} {size} jf-{justify}"
|
||||
class:only-icon={iconOnly}
|
||||
|
@ -22,9 +22,11 @@
|
||||
export let transparent: boolean = false
|
||||
export let selected: boolean = false
|
||||
export let primary: boolean = false
|
||||
export let id: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div
|
||||
{id}
|
||||
class="flex-center icon-button icon-{size}"
|
||||
class:selected
|
||||
class:transparent
|
||||
|
@ -15,11 +15,10 @@
|
||||
<script lang="ts">
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '../types'
|
||||
|
||||
// import Icon from './Icon.svelte'
|
||||
import Loading from './Loading.svelte'
|
||||
import ErrorBoundary from './internal/ErrorBoundary'
|
||||
import ErrorPresenter from './ErrorPresenter.svelte'
|
||||
import ErrorBoundary from './internal/ErrorBoundary'
|
||||
import Loading from './Loading.svelte'
|
||||
|
||||
export let is: AnyComponent
|
||||
export let props = {}
|
||||
|
@ -19,7 +19,6 @@
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let size: IconSize
|
||||
export let fill = 'currentColor'
|
||||
export let filled: boolean = false
|
||||
|
||||
function isAsset (icon: Asset | AnySvelteComponent): boolean {
|
||||
return typeof icon === 'string'
|
||||
@ -38,5 +37,5 @@
|
||||
<use href={url} />
|
||||
</svg>
|
||||
{:else}
|
||||
<svelte:component this={icon} {size} {fill} {filled} />
|
||||
<svelte:component this={icon} {size} {fill} />
|
||||
{/if}
|
||||
|
@ -13,20 +13,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { ticker } from '..'
|
||||
import ui from '../plugin'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let value: number
|
||||
|
||||
<script lang="ts" context="module">
|
||||
const SECOND = 1000
|
||||
const MINUTE = SECOND * 60
|
||||
const HOUR = MINUTE * 60
|
||||
const DAY = HOUR * 24
|
||||
const MONTH = DAY * 30
|
||||
const YEAR = MONTH * 12
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { ticker } from '..'
|
||||
import ui from '../plugin'
|
||||
import { tooltip } from '../tooltips'
|
||||
|
||||
export let value: number
|
||||
|
||||
let time: string = ''
|
||||
|
||||
@ -48,7 +50,7 @@
|
||||
|
||||
$: formatTime($ticker)
|
||||
|
||||
$: tooltip = new Date(value).toLocaleString('default', {
|
||||
$: tooltipValue = new Date(value).toLocaleString('default', {
|
||||
minute: '2-digit',
|
||||
hour: 'numeric',
|
||||
day: '2-digit',
|
||||
@ -57,6 +59,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<Tooltip label={ui.string.TimeTooltip} props={{ value: tooltip }}>
|
||||
<span style="white-space: nowrap;">{time}</span>
|
||||
</Tooltip>
|
||||
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: tooltipValue } }} style="white-space: nowrap;">
|
||||
{time}
|
||||
</span>
|
||||
|
@ -11,8 +11,41 @@ const emptyTooltip: LabelAndProps = {
|
||||
anchor: undefined,
|
||||
onUpdate: undefined
|
||||
}
|
||||
let storedValue: LabelAndProps = emptyTooltip
|
||||
export const tooltipstore = writable<LabelAndProps>(emptyTooltip)
|
||||
|
||||
export function tooltip (node: HTMLElement, options?: LabelAndProps): any {
|
||||
let toHandler: any
|
||||
if (options === undefined) {
|
||||
return {}
|
||||
}
|
||||
let opt = options
|
||||
const show = (): void => {
|
||||
const shown = !!(storedValue.label !== undefined || storedValue.component !== undefined)
|
||||
if (!shown) {
|
||||
clearTimeout(toHandler)
|
||||
toHandler = setTimeout(() => {
|
||||
showTooltip(opt.label, node, opt.direction, opt.component, opt.props, opt.anchor, opt.onUpdate)
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
const hide = (): void => {
|
||||
clearTimeout(toHandler)
|
||||
}
|
||||
node.addEventListener('mouseleave', hide)
|
||||
node.addEventListener('mousemove', show)
|
||||
return {
|
||||
update (options: LabelAndProps) {
|
||||
opt = options
|
||||
},
|
||||
|
||||
destroy () {
|
||||
node.removeEventListener('mousemove', show)
|
||||
node.removeEventListener('mouseleave', hide)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showTooltip (
|
||||
label: IntlString | undefined,
|
||||
element: HTMLElement,
|
||||
@ -22,7 +55,7 @@ export function showTooltip (
|
||||
anchor?: HTMLElement,
|
||||
onUpdate?: (result: any) => void
|
||||
): void {
|
||||
tooltipstore.set({
|
||||
storedValue = {
|
||||
label: label,
|
||||
element: element,
|
||||
direction: direction,
|
||||
@ -30,9 +63,11 @@ export function showTooltip (
|
||||
props: props,
|
||||
anchor: anchor,
|
||||
onUpdate: onUpdate
|
||||
})
|
||||
}
|
||||
tooltipstore.set(storedValue)
|
||||
}
|
||||
|
||||
export function closeTooltip (): void {
|
||||
storedValue = emptyTooltip
|
||||
tooltipstore.set(emptyTooltip)
|
||||
}
|
||||
|
@ -113,12 +113,12 @@ export interface DateOrShift {
|
||||
}
|
||||
|
||||
export interface LabelAndProps {
|
||||
label: IntlString | undefined
|
||||
element: HTMLElement | undefined
|
||||
label?: IntlString
|
||||
element?: HTMLElement
|
||||
direction?: TooltipAlignment
|
||||
component?: AnySvelteComponent | AnyComponent
|
||||
props?: any
|
||||
anchor: HTMLElement | undefined
|
||||
anchor?: HTMLElement
|
||||
onUpdate?: (result: any) => void
|
||||
}
|
||||
|
||||
|
@ -15,9 +15,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Doc } from '@anticrm/core'
|
||||
import { Tooltip, IconAttachment } from '@anticrm/ui'
|
||||
import AttachmentPopup from './AttachmentPopup.svelte'
|
||||
import { IconAttachment, tooltip } from '@anticrm/ui'
|
||||
import attachment from '../plugin'
|
||||
import AttachmentPopup from './AttachmentPopup.svelte'
|
||||
|
||||
export let value: number | undefined
|
||||
export let object: Doc
|
||||
@ -26,16 +26,17 @@
|
||||
</script>
|
||||
|
||||
{#if value && value > 0}
|
||||
<Tooltip
|
||||
label={attachment.string.Attachments}
|
||||
component={AttachmentPopup}
|
||||
props={{ objectId: object._id, attachments: value }}
|
||||
<div
|
||||
use:tooltip={{
|
||||
label: attachment.string.Attachments,
|
||||
component: AttachmentPopup,
|
||||
props: { objectId: object._id, attachments: value }
|
||||
}}
|
||||
class="sm-tool-icon ml-1 mr-1"
|
||||
>
|
||||
<div class="sm-tool-icon ml-1 mr-1">
|
||||
<span class="icon"><IconAttachment {size} /></span>
|
||||
{#if showCounter}
|
||||
{value}
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<span class="icon"><IconAttachment {size} /></span>
|
||||
{#if showCounter}
|
||||
{value}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -14,10 +14,10 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Doc } from '@anticrm/core'
|
||||
import { Tooltip, IconThread } from '@anticrm/ui'
|
||||
import CommentPopup from './CommentPopup.svelte'
|
||||
import chunter from '@anticrm/chunter'
|
||||
import type { Doc } from '@anticrm/core'
|
||||
import { IconThread, tooltip } from '@anticrm/ui'
|
||||
import CommentPopup from './CommentPopup.svelte'
|
||||
|
||||
export let value: number | undefined
|
||||
export let object: Doc
|
||||
@ -26,12 +26,17 @@
|
||||
</script>
|
||||
|
||||
{#if value && value > 0}
|
||||
<Tooltip label={chunter.string.Comments} component={CommentPopup} props={{ objectId: object._id }}>
|
||||
<div class="sm-tool-icon ml-1 mr-1">
|
||||
<span class="icon"><IconThread {size} /></span>
|
||||
{#if showCounter}
|
||||
{value}
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
use:tooltip={{
|
||||
label: chunter.string.Comments,
|
||||
component: CommentPopup,
|
||||
props: { objectId: object._id }
|
||||
}}
|
||||
class="sm-tool-icon ml-1 mr-1"
|
||||
>
|
||||
<span class="icon"><IconThread {size} /></span>
|
||||
{#if showCounter}
|
||||
{value}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -59,6 +59,10 @@
|
||||
"StatusDueDateTooltip": "Until {date}",
|
||||
"CopyToClipboard": "Copy to clipboard",
|
||||
"Copied": "Copied",
|
||||
"ViewFullProfile": "View full profile"
|
||||
"ViewFullProfile": "View full profile",
|
||||
"Member": "Member",
|
||||
"Members": "Members",
|
||||
"NoMembers": "No members added",
|
||||
"AddMember": "Add member"
|
||||
}
|
||||
}
|
@ -59,6 +59,10 @@
|
||||
"StatusDueDateTooltip": "До {date}",
|
||||
"CopyToClipboard": "Скопировать в буфер обмена",
|
||||
"Copied": "Скопировано",
|
||||
"ViewFullProfile": "Посмотреть профиль"
|
||||
"ViewFullProfile": "Посмотреть профиль",
|
||||
"Member": "Сотрудник",
|
||||
"Members": "Сотрудники",
|
||||
"NoMembers": "Нет добавленных сотрудников",
|
||||
"AddMember": "Добавить сотрудника"
|
||||
}
|
||||
}
|
@ -223,43 +223,42 @@
|
||||
class:short={displayItems.length > 4 && length === 'short'}
|
||||
>
|
||||
{#each displayItems as item, i}
|
||||
<Tooltip
|
||||
component={opened !== i ? ChannelEditor : undefined}
|
||||
props={{
|
||||
value: item.value,
|
||||
placeholder: item.placeholder,
|
||||
editable: editable !== undefined ? false : undefined,
|
||||
openable: item.presenter ?? false
|
||||
}}
|
||||
onUpdate={(result) => {
|
||||
if (result.detail === 'open') {
|
||||
<Button
|
||||
focusIndex={focusIndex === -1 ? focusIndex : focusIndex + 1 + i}
|
||||
id={item.label}
|
||||
bind:input={btns[i]}
|
||||
icon={item.icon}
|
||||
kind={highlighted.includes(item.provider) ? 'dangerous' : kind}
|
||||
{size}
|
||||
{shape}
|
||||
highlight={item.integration || item.notification}
|
||||
on:click={(ev) => {
|
||||
if (editable) {
|
||||
closeTooltip()
|
||||
editChannel(eventToHTMLElement(ev), i, item)
|
||||
} else {
|
||||
dispatch('open', item)
|
||||
} else if (result.detail === 'edit') {
|
||||
closeTooltip()
|
||||
editChannel(btns[i], i, item)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
focusIndex={focusIndex === -1 ? focusIndex : focusIndex + 1 + i}
|
||||
id={item.label}
|
||||
bind:input={btns[i]}
|
||||
icon={item.icon}
|
||||
kind={highlighted.includes(item.provider) ? 'dangerous' : kind}
|
||||
{size}
|
||||
{shape}
|
||||
highlight={item.integration || item.notification}
|
||||
on:click={(ev) => {
|
||||
if (editable) {
|
||||
showTooltip={{
|
||||
component: opened !== i ? ChannelEditor : undefined,
|
||||
props: {
|
||||
value: item.value,
|
||||
placeholder: item.placeholder,
|
||||
editable: editable !== undefined ? false : undefined,
|
||||
openable: item.presenter ?? false
|
||||
},
|
||||
onUpdate: (result) => {
|
||||
if (result.detail === 'open') {
|
||||
closeTooltip()
|
||||
editChannel(eventToHTMLElement(ev), i, item)
|
||||
} else {
|
||||
dispatch('open', item)
|
||||
} else if (result.detail === 'edit') {
|
||||
closeTooltip()
|
||||
editChannel(btns[i], i, item)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#if actions.length > 0 && editable}
|
||||
{#if displayItems.length === 0}
|
||||
|
@ -15,9 +15,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Channel } from '@anticrm/contact'
|
||||
import { Doc } from '@anticrm/core'
|
||||
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import ChannelsDropdown from './ChannelsDropdown.svelte'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import ChannelsDropdown from './ChannelsDropdown.svelte'
|
||||
|
||||
export let value: Channel[] | Channel | null
|
||||
|
||||
@ -26,12 +27,13 @@
|
||||
export let size: ButtonSize = 'small'
|
||||
export let length: 'short' | 'full' = 'short'
|
||||
export let shape: 'circle' | undefined = 'circle'
|
||||
export let object: Doc
|
||||
|
||||
function _open (ev: any) {
|
||||
if (ev.detail.presenter !== undefined && Array.isArray(value)) {
|
||||
const channel = value[0]
|
||||
if (channel !== undefined) {
|
||||
showPopup(ev.detail.presenter, { _id: channel.attachedTo, _class: channel.attachedToClass }, 'float')
|
||||
showPopup(ev.detail.presenter, { _id: object._id, _class: object._class }, 'float')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,23 +14,25 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Contact } from '@anticrm/contact'
|
||||
import contact, { Contact, Organization } from '@anticrm/contact'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
|
||||
import OrganizationPresenter from './OrganizationPresenter.svelte'
|
||||
import PersonPresenter from './PersonPresenter.svelte'
|
||||
|
||||
export let value: Contact
|
||||
export let isInteractive = true
|
||||
|
||||
function isPerson (value: Contact): boolean {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
return hierarchy.isDerived(value._class, contact.class.Person)
|
||||
}
|
||||
const toOrg = (contact: Contact) => contact as Organization
|
||||
</script>
|
||||
|
||||
{#if isPerson(value)}
|
||||
<PersonPresenter {value} />
|
||||
<PersonPresenter {isInteractive} {value} />
|
||||
{:else}
|
||||
<OrganizationPresenter {value} />
|
||||
<OrganizationPresenter value={toOrg(value)} />
|
||||
{/if}
|
||||
|
76
plugins/contact-resources/src/components/CreateMember.svelte
Normal file
76
plugins/contact-resources/src/components/CreateMember.svelte
Normal file
@ -0,0 +1,76 @@
|
||||
<!--
|
||||
// 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 { Organization, Person } from '@anticrm/contact'
|
||||
import { Class, Ref, Space } from '@anticrm/core'
|
||||
import { Card, getClient, UserBox } from '@anticrm/presentation'
|
||||
import { createFocusManager, FocusHandler } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let organization: Ref<Organization>
|
||||
export let _class: Ref<Class<Organization>>
|
||||
export let space: Ref<Space>
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let person: Ref<Person> | undefined
|
||||
|
||||
export function canClose (): boolean {
|
||||
return person === undefined
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function createMember () {
|
||||
if (person === undefined) {
|
||||
return
|
||||
}
|
||||
const id = await client.addCollection(contact.class.Member, space, organization, _class, 'members', {
|
||||
contact: person
|
||||
})
|
||||
|
||||
dispatch('close', id)
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
<Card
|
||||
label={contact.string.AddMember}
|
||||
okAction={createMember}
|
||||
canSave={!!person}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center clear-mins">
|
||||
<!-- <div class="mr-3">
|
||||
<Button focusIndex={1} icon={Vacancy} size={'medium'} kind={'link-bordered'} disabled />
|
||||
</div> -->
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<UserBox
|
||||
focusIndex={3}
|
||||
_class={contact.class.Person}
|
||||
label={contact.string.Member}
|
||||
placeholder={contact.string.Member}
|
||||
bind:value={person}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
icon={contact.icon.Person}
|
||||
create={{ component: contact.component.CreatePerson, label: contact.string.CreatePerson }}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
66
plugins/contact-resources/src/components/EditMember.svelte
Normal file
66
plugins/contact-resources/src/components/EditMember.svelte
Normal file
@ -0,0 +1,66 @@
|
||||
<!--
|
||||
// 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 contact, { Contact, Member, Organization } from '@anticrm/contact'
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
|
||||
import OrganizationCard from './OrganizationCard.svelte'
|
||||
import PersonCard from './PersonCard.svelte'
|
||||
|
||||
export let object: Member
|
||||
|
||||
let refContact: Contact
|
||||
const personQuery = createQuery()
|
||||
$: if (object !== undefined) {
|
||||
personQuery.query(contact.class.Contact, { _id: object.contact }, (result) => {
|
||||
refContact = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
let organization: Organization
|
||||
const orgQuery = createQuery()
|
||||
$: if (object !== undefined) {
|
||||
orgQuery.query(contact.class.Organization, { _id: object.attachedTo as Ref<Organization> }, (result) => {
|
||||
organization = result[0]
|
||||
})
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['comments', 'number'] })
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if object !== undefined && refContact !== undefined && organization !== undefined}
|
||||
<div class="flex-between">
|
||||
<div class="card"><PersonCard object={refContact} on:click /></div>
|
||||
<div class="arrows"><ExpandRightDouble /></div>
|
||||
<div class="card"><OrganizationCard {organization} /></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
align-self: stretch;
|
||||
width: calc(50% - 3rem);
|
||||
min-height: 16rem;
|
||||
}
|
||||
.arrows {
|
||||
width: 4rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,35 @@
|
||||
<!--
|
||||
// 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 contact, { Member } from '@anticrm/contact'
|
||||
import { Hierarchy } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { getPanelURI } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { ContactPresenter } from '..'
|
||||
|
||||
export let value: Member
|
||||
|
||||
const contactRef = getClient().findOne(contact.class.Contact, { _id: value.contact })
|
||||
</script>
|
||||
|
||||
<a href={`#${getPanelURI(view.component.EditDoc, value._id, Hierarchy.mixinOrClass(value), 'content')}`}>
|
||||
{#await contactRef then ct}
|
||||
{#if ct}
|
||||
<ContactPresenter isInteractive={false} value={ct} />
|
||||
{/if}
|
||||
{/await}
|
||||
</a>
|
143
plugins/contact-resources/src/components/Members.svelte
Normal file
143
plugins/contact-resources/src/components/Members.svelte
Normal file
@ -0,0 +1,143 @@
|
||||
<!--
|
||||
// Copyright © 2022 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { Member } from '@anticrm/contact'
|
||||
import type { Class, Doc, Ref, Space } from '@anticrm/core'
|
||||
import { createQuery, getClient, UsersPopup } from '@anticrm/presentation'
|
||||
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
|
||||
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
|
||||
import { Table, ViewletSettingButton } from '@anticrm/view-resources'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let objectId: Ref<Doc>
|
||||
export let space: Ref<Space>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
|
||||
export let members: number
|
||||
|
||||
let memberItems: Member[] = []
|
||||
|
||||
const membersQuery = createQuery()
|
||||
$: membersQuery.query(contact.class.Member, { attachedTo: objectId }, (result) => {
|
||||
memberItems = result
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
let loading = true
|
||||
|
||||
const createApp = async (ev: MouseEvent): Promise<void> => {
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
_class: contact.class.Person,
|
||||
options: undefined,
|
||||
ignoreUsers: memberItems.map((it) => it.contact),
|
||||
icon: contact.icon.Person,
|
||||
allowDeselect: false,
|
||||
placeholder: contact.string.Member,
|
||||
create: { component: contact.component.CreatePerson, label: contact.string.CreatePerson }
|
||||
},
|
||||
ev.target as HTMLElement,
|
||||
(result) => {
|
||||
if (result != null) {
|
||||
client.addCollection(contact.class.Member, space, objectId, _class, 'members', {
|
||||
contact: result._id
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let descr: Viewlet | undefined
|
||||
|
||||
const preferenceQuery = createQuery()
|
||||
let preference: ViewletPreference | undefined
|
||||
|
||||
$: updateDescriptor(contact.viewlet.TableMember)
|
||||
|
||||
function updateDescriptor (id: Ref<Viewlet>) {
|
||||
loading = true
|
||||
client
|
||||
.findOne<Viewlet>(view.class.Viewlet, {
|
||||
_id: id
|
||||
})
|
||||
.then((res) => {
|
||||
descr = res
|
||||
if (res !== undefined) {
|
||||
preferenceQuery.query(
|
||||
view.class.ViewletPreference,
|
||||
{
|
||||
attachedTo: res._id
|
||||
},
|
||||
(res) => {
|
||||
preference = res[0]
|
||||
loading = false
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="applications-container">
|
||||
<div class="flex flex-between">
|
||||
<div class="title"><Label label={contact.string.Members} /></div>
|
||||
<CircleButton id={contact.string.AddMember} icon={IconAdd} size={'small'} selected on:click={createApp} />
|
||||
<div class="flex flex-grow flex-reverse">
|
||||
<ViewletSettingButton viewlet={descr} />
|
||||
</div>
|
||||
</div>
|
||||
{#if members > 0 && descr}
|
||||
<Table
|
||||
_class={contact.class.Member}
|
||||
config={preference?.config ?? descr.config}
|
||||
options={descr.options}
|
||||
query={{ attachedTo: objectId }}
|
||||
loadingProps={{ length: members }}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-col-center mt-5 createapp-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={createApp}><Label label={contact.string.AddMember} /></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.applications-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
margin-right: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.createapp-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>
|
@ -0,0 +1,95 @@
|
||||
<!--
|
||||
// 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 { Organization } from '@anticrm/contact'
|
||||
import { Avatar } from '@anticrm/presentation'
|
||||
import { closePanel, closePopup, closeTooltip, getCurrentLocation, Label, navigate } from '@anticrm/ui'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let organization: Organization
|
||||
export let disabled: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="flex-col h-full card-container">
|
||||
<div class="label uppercase"><Label label={contact.string.Organization} /></div>
|
||||
<div class="flex-center logo">
|
||||
<Avatar avatar={organization.avatar} size={'large'} icon={contact.icon.Company} />
|
||||
</div>
|
||||
{#if organization}
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
on:click={() => {
|
||||
if (!disabled) {
|
||||
closeTooltip()
|
||||
closePopup()
|
||||
closePanel()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = organization._id
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{organization.name}
|
||||
</div>
|
||||
{#if organization}
|
||||
<span class="label">{organization.name}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
padding: 1rem 1.5rem 1.25rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 0.5rem;
|
||||
transition-property: box-shadow, background-color, border-color;
|
||||
transition-timing-function: var(--timing-shadow);
|
||||
transition-duration: 0.15s;
|
||||
user-select: text;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
border-color: var(--button-border-color);
|
||||
box-shadow: var(--accent-shadow);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.label {
|
||||
margin-bottom: 1.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.625rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.name {
|
||||
margin: 1rem 0 0.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.description {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
}
|
||||
</style>
|
114
plugins/contact-resources/src/components/PersonCard.svelte
Normal file
114
plugins/contact-resources/src/components/PersonCard.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<!--
|
||||
// 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 attachment from '@anticrm/attachment'
|
||||
import contact, { Channel, Contact, formatName } from '@anticrm/contact'
|
||||
import { Avatar, createQuery } from '@anticrm/presentation'
|
||||
import { Component, Label, showPanel } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import ChannelsEditor from './ChannelsEditor.svelte'
|
||||
|
||||
export let object: Contact
|
||||
export let disabled: boolean = false
|
||||
|
||||
let channels: Channel[] = []
|
||||
const channelsQuery = createQuery()
|
||||
channelsQuery.query(
|
||||
contact.class.Channel,
|
||||
{
|
||||
attachedTo: object._id
|
||||
},
|
||||
(res) => {
|
||||
channels = res
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex-col h-full card-container">
|
||||
<div class="label uppercase"><Label label={contact.string.Person} /></div>
|
||||
<div class="flex-center logo">
|
||||
<Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} />
|
||||
</div>
|
||||
{#if object}
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
on:click={() => {
|
||||
if (!disabled) showPanel(view.component.EditDoc, object._id, object._class, 'content')
|
||||
}}
|
||||
>
|
||||
{formatName(object.name)}
|
||||
</div>
|
||||
<div class="description overflow-label">{object.city ?? ''}</div>
|
||||
<div class="footer flex flex-reverse flex-grow">
|
||||
<div class="flex-center flex-wrap">
|
||||
<Component
|
||||
is={attachment.component.AttachmentsPresenter}
|
||||
props={{ value: object.attachments, object: object, size: 'medium', showCounter: true }}
|
||||
/>
|
||||
</div>
|
||||
{#if channels[0]}
|
||||
<div class="flex flex-grow">
|
||||
<ChannelsEditor
|
||||
attachedTo={channels[0].attachedTo}
|
||||
attachedClass={channels[0].attachedToClass}
|
||||
length={'short'}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
padding: 1rem 1.5rem 1.25rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 0.5rem;
|
||||
transition-property: box-shadow, background-color, border-color;
|
||||
transition-timing-function: var(--timing-shadow);
|
||||
transition-duration: 0.15s;
|
||||
user-select: text;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
border-color: var(--button-border-color);
|
||||
box-shadow: var(--accent-shadow);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 1.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.625rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.name {
|
||||
margin: 1rem 0 0.25rem;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.description {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.footer {
|
||||
margin-top: 1.5rem;
|
||||
// overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -17,7 +17,7 @@
|
||||
import { Hierarchy } from '@anticrm/core'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { Avatar } from '@anticrm/presentation'
|
||||
import { getPanelURI, Label } from '@anticrm/ui'
|
||||
import { getPanelURI, Label, LabelAndProps, tooltip } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
|
||||
export let value: Person | undefined
|
||||
@ -29,6 +29,7 @@
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
export let avatarSize: 'inline' | 'tiny' | 'x-small' | 'small' | 'medium' | 'large' | 'x-large' = 'x-small'
|
||||
export let onEdit: ((event: MouseEvent) => void) | undefined = undefined
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
|
||||
$: element = getElement(value, onEdit, shouldShowPlaceholder, isInteractive)
|
||||
|
||||
@ -56,6 +57,7 @@
|
||||
|
||||
<svelte:element
|
||||
this={element}
|
||||
use:tooltip={showTooltip}
|
||||
class="contentPresenter"
|
||||
class:inline-presenter={inline}
|
||||
class:mContentPresenterNotInteractive={!isInteractive}
|
||||
|
@ -15,7 +15,6 @@
|
||||
<script lang="ts">
|
||||
import { formatName, Person } from '@anticrm/contact'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import PersonContent from './PersonContent.svelte'
|
||||
|
||||
export let value: Person
|
||||
@ -31,34 +30,21 @@
|
||||
</script>
|
||||
|
||||
{#if value || shouldShowPlaceholder}
|
||||
{#if tooltipLabels}
|
||||
<Tooltip
|
||||
label={value ? tooltipLabels.personLabel : tooltipLabels.placeholderLabel}
|
||||
props={{ value: formatName(value?.name) }}
|
||||
>
|
||||
<PersonContent
|
||||
{value}
|
||||
{inline}
|
||||
{onEdit}
|
||||
{avatarSize}
|
||||
{defaultName}
|
||||
{isInteractive}
|
||||
{shouldShowAvatar}
|
||||
{shouldShowName}
|
||||
{shouldShowPlaceholder}
|
||||
/>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<PersonContent
|
||||
{value}
|
||||
{inline}
|
||||
{onEdit}
|
||||
{avatarSize}
|
||||
{defaultName}
|
||||
{isInteractive}
|
||||
{shouldShowAvatar}
|
||||
{shouldShowName}
|
||||
{shouldShowPlaceholder}
|
||||
/>
|
||||
{/if}
|
||||
<PersonContent
|
||||
showTooltip={tooltipLabels
|
||||
? {
|
||||
label: value ? tooltipLabels.personLabel : tooltipLabels.placeholderLabel,
|
||||
props: { value: formatName(value?.name) }
|
||||
}
|
||||
: undefined}
|
||||
{value}
|
||||
{inline}
|
||||
{onEdit}
|
||||
{avatarSize}
|
||||
{defaultName}
|
||||
{isInteractive}
|
||||
{shouldShowAvatar}
|
||||
{shouldShowName}
|
||||
{shouldShowPlaceholder}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<!--
|
||||
// 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">
|
||||
const fill: string = 'var(--theme-content-dark-color)'
|
||||
</script>
|
||||
|
||||
<svg class="svg-expand" {fill} viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="32.2,15.6 31.4,16.4 46.9,32 31.4,47.6 32.2,48.4 48.6,32 " />
|
||||
<polygon points="32.6,32 16.2,15.6 15.4,16.4 30.9,32 15.4,47.6 16.2,48.4 " />
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
.svg-expand {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
</style>
|
@ -40,6 +40,9 @@ import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svel
|
||||
import OrganizationEditor from './components/OrganizationEditor.svelte'
|
||||
import PersonEditor from './components/PersonEditor.svelte'
|
||||
import OrganizationSelector from './components/OrganizationSelector.svelte'
|
||||
import Members from './components/Members.svelte'
|
||||
import MemberPresenter from './components/MemberPresenter.svelte'
|
||||
import EditMember from './components/EditMember.svelte'
|
||||
|
||||
export {
|
||||
Channels,
|
||||
@ -48,7 +51,8 @@ export {
|
||||
ChannelsView,
|
||||
OrganizationSelector,
|
||||
ChannelsDropdown,
|
||||
EmployeePresenter
|
||||
EmployeePresenter,
|
||||
MemberPresenter
|
||||
}
|
||||
|
||||
async function queryContact (
|
||||
@ -83,7 +87,10 @@ export default async (): Promise<Resources> => ({
|
||||
SocialEditor,
|
||||
Contacts,
|
||||
EmployeeAccountPresenter,
|
||||
EmployeePresenter
|
||||
EmployeePresenter,
|
||||
Members,
|
||||
MemberPresenter,
|
||||
EditMember
|
||||
},
|
||||
completion: {
|
||||
EmployeeQuery: async (client: Client, query: string) => await queryContact(contact.class.Employee, client, query),
|
||||
|
@ -52,6 +52,10 @@ export default mergeIds(contactId, contact, {
|
||||
StatusDueDateTooltip: '' as IntlString,
|
||||
CopyToClipboard: '' as IntlString,
|
||||
Copied: '' as IntlString,
|
||||
ViewFullProfile: '' as IntlString
|
||||
ViewFullProfile: '' as IntlString,
|
||||
Member: '' as IntlString,
|
||||
Members: '' as IntlString,
|
||||
NoMembers: '' as IntlString,
|
||||
AddMember: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -28,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@anticrm/platform": "~0.6.6",
|
||||
"@anticrm/ui": "~0.6.0",
|
||||
"@anticrm/core": "~0.6.16"
|
||||
"@anticrm/core": "~0.6.16",
|
||||
"@anticrm/view": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
import { IntlString, plugin } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import { Viewlet } from '@anticrm/view'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -80,7 +81,15 @@ export interface Person extends Contact {}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Organization extends Contact {}
|
||||
export interface Member extends AttachedDoc {
|
||||
contact: Ref<Contact>
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Organization extends Contact {
|
||||
members: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -152,6 +161,7 @@ const contactPlugin = plugin(contactId, {
|
||||
Contact: '' as Ref<Class<Contact>>,
|
||||
Person: '' as Ref<Class<Person>>,
|
||||
Persons: '' as Ref<Class<Persons>>,
|
||||
Member: '' as Ref<Class<Member>>,
|
||||
Organization: '' as Ref<Class<Organization>>,
|
||||
Organizations: '' as Ref<Class<Organizations>>,
|
||||
Employee: '' as Ref<Class<Employee>>,
|
||||
@ -160,7 +170,8 @@ const contactPlugin = plugin(contactId, {
|
||||
},
|
||||
component: {
|
||||
SocialEditor: '' as AnyComponent,
|
||||
CreateOrganization: '' as AnyComponent
|
||||
CreateOrganization: '' as AnyComponent,
|
||||
CreatePerson: '' as AnyComponent
|
||||
},
|
||||
channelProvider: {
|
||||
Email: '' as Ref<ChannelProvider>,
|
||||
@ -202,6 +213,9 @@ const contactPlugin = plugin(contactId, {
|
||||
PersonAlreadyExists: '' as IntlString,
|
||||
Person: '' as IntlString,
|
||||
CreateOrganization: '' as IntlString
|
||||
},
|
||||
viewlet: {
|
||||
TableMember: '' as Ref<Viewlet>
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -15,18 +15,23 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Candidate } from '@anticrm/recruit'
|
||||
import { Icon, Tooltip } from '@anticrm/ui'
|
||||
import ApplicationsPopup from './ApplicationsPopup.svelte'
|
||||
import { Icon, tooltip } from '@anticrm/ui'
|
||||
import recruit from '../plugin'
|
||||
import ApplicationsPopup from './ApplicationsPopup.svelte'
|
||||
|
||||
export let value: number
|
||||
export let object: Candidate
|
||||
</script>
|
||||
|
||||
{#if value && value > 0}
|
||||
<Tooltip label={recruit.string.Applications} component={ApplicationsPopup} props={{ value: object }}>
|
||||
<div class="sm-tool-icon">
|
||||
<span class="icon"><Icon icon={recruit.icon.Application} size={'small'} /></span> {value}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
use:tooltip={{
|
||||
label: recruit.string.Applications,
|
||||
component: ApplicationsPopup,
|
||||
props: { value: object }
|
||||
}}
|
||||
class="sm-tool-icon"
|
||||
>
|
||||
<span class="icon"><Icon icon={recruit.icon.Application} size={'small'} /></span> {value}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Class, Doc, Ref } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Icon, Tooltip } from '@anticrm/ui'
|
||||
import { Icon, tooltip } from '@anticrm/ui'
|
||||
import { getCollectionCounter } from '@anticrm/view-resources'
|
||||
import tagsId from '../plugin'
|
||||
import TagsPresentationPopup from './TagsPresentationPopup.svelte'
|
||||
@ -31,9 +31,14 @@
|
||||
</script>
|
||||
|
||||
{#if tags > 0}
|
||||
<Tooltip label={attr.label} component={TagsPresentationPopup} props={{ object: value, _class, key: { key, attr } }}>
|
||||
<div class="sm-tool-icon">
|
||||
<span class="icon"><Icon icon={tagsId.icon.Tags} size={'small'} /></span> {tags}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
use:tooltip={{
|
||||
label: attr.label,
|
||||
component: TagsPresentationPopup,
|
||||
props: { object: value, _class, key: { key, attr } }
|
||||
}}
|
||||
class="sm-tool-icon"
|
||||
>
|
||||
<span class="icon"><Icon icon={tagsId.icon.Tags} size={'small'} /></span> {tags}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -153,6 +153,13 @@
|
||||
r?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
|
||||
const joinProps = (collectionAttr: boolean, object: Doc, props: any) => {
|
||||
if (collectionAttr) {
|
||||
return { object, ...props }
|
||||
}
|
||||
return props
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: config, lookup })}
|
||||
@ -251,8 +258,7 @@
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getObjectValue(attribute.key, object) ?? ''}
|
||||
{object}
|
||||
{...attribute.props}
|
||||
{...joinProps(attribute.collectionAttr, object, attribute.props)}
|
||||
/>
|
||||
<!-- <div
|
||||
id="context-menu"
|
||||
@ -268,8 +274,7 @@
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getObjectValue(attribute.key, object) ?? ''}
|
||||
{object}
|
||||
{...attribute.props}
|
||||
{...joinProps(attribute.collectionAttr, object, attribute.props)}
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
|
@ -75,7 +75,8 @@ export async function getObjectPresenter (
|
||||
label: preserveKey.label ?? clazz.label,
|
||||
presenter,
|
||||
props: preserveKey.props,
|
||||
sortingKey
|
||||
sortingKey,
|
||||
collectionAttr: isCollectionAttr
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,9 +103,8 @@ async function getAttributePresenter (
|
||||
const hierarchy = client.getHierarchy()
|
||||
const attribute = hierarchy.getAttribute(_class, key)
|
||||
let attrClass = getAttributePresenterClass(attribute)
|
||||
const mixin = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||
? view.mixin.CollectionPresenter
|
||||
: view.mixin.AttributePresenter
|
||||
const isCollectionAttr = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.AttributePresenter
|
||||
const clazz = hierarchy.getClass(attrClass)
|
||||
let presenterMixin = hierarchy.as(clazz, mixin)
|
||||
let parent = clazz.extends
|
||||
@ -129,7 +129,8 @@ async function getAttributePresenter (
|
||||
presenter,
|
||||
props: {},
|
||||
icon: presenterMixin.icon,
|
||||
attribute
|
||||
attribute,
|
||||
collectionAttr: isCollectionAttr
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +150,8 @@ export async function getPresenter<T extends Doc> (
|
||||
_class,
|
||||
label: label as IntlString,
|
||||
presenter: await getResource(presenter),
|
||||
props: key.props
|
||||
props: key.props,
|
||||
collectionAttr: isCollectionAttr
|
||||
}
|
||||
}
|
||||
if (key.key.length === 0) {
|
||||
@ -233,12 +235,13 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
|
||||
presenter: ErrorPresenter,
|
||||
label: stringKey as IntlString,
|
||||
_class: core.class.TypeString,
|
||||
props: { error: err }
|
||||
props: { error: err },
|
||||
collectionAttr: false
|
||||
}
|
||||
return errorPresenter
|
||||
}
|
||||
})
|
||||
return (await Promise.all(model)).filter((a) => a !== undefined) as AttributeModel[]
|
||||
return (await (await Promise.all(model)).filter((a) => a !== undefined)) as AttributeModel[]
|
||||
}
|
||||
|
||||
export async function deleteObject (client: TxOperations, object: Doc): Promise<void> {
|
||||
|
@ -325,6 +325,7 @@ export interface AttributeModel {
|
||||
icon?: Asset
|
||||
|
||||
attribute?: AnyAttribute
|
||||
collectionAttr: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
29
tests/sanity/tests/org.members.spec.ts
Normal file
29
tests/sanity/tests/org.members.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { generateId, PlatformSetting, PlatformURI } from './utils'
|
||||
|
||||
test.use({
|
||||
storageState: PlatformSetting
|
||||
})
|
||||
|
||||
test.describe('recruit tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Create user and workspace
|
||||
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
|
||||
})
|
||||
|
||||
test('org-add-member', async ({ page }) => {
|
||||
await page.click('[id="app-contact\\:string\\:Contacts"]')
|
||||
await page.click('button:has-text("Contact")')
|
||||
await page.click('button:has-text("Organization")')
|
||||
await page.click('[placeholder="Apple"]')
|
||||
const orgId = 'Organiation-' + generateId()
|
||||
await page.fill('[placeholder="Apple"]', orgId)
|
||||
await page.click('button:has-text("Create")')
|
||||
await page.click(`text=${orgId}`)
|
||||
|
||||
await page.click('[id="contact:string:AddMember"]')
|
||||
await page.click('button:has-text("Rosamund Chen")')
|
||||
await page.click('text=Rosamund Chen less than a minute ago >> span')
|
||||
await page.click(`:nth-match(:text("${orgId}"), 2)`)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user