mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-28 22:13:29 +03:00
Add ChannelsDropdown (#1459)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
30095a6929
commit
6df202f166
@ -49,6 +49,7 @@ input {
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
color: var(--caption-color);
|
||||
&::placeholder { color: var(--dark-color); }
|
||||
&.wrong-input { background-color: var(--system-error-color) !important; }
|
||||
}
|
||||
audio, canvas, embed, iframe, img, object, svg, video {
|
||||
|
@ -42,8 +42,29 @@
|
||||
color: #d6d6d6;
|
||||
border: none;
|
||||
caret-color: var(--caret-color);
|
||||
|
||||
&::placeholder { color: var(--content-color); }
|
||||
}
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
border-radius: 50%;
|
||||
|
||||
.icon {
|
||||
width: .625rem;
|
||||
height: .625rem;
|
||||
}
|
||||
|
||||
&.show {
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-border-color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-border-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +101,7 @@
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
.color {
|
||||
width: .875rem;
|
||||
@ -105,7 +127,11 @@
|
||||
margin-right: .75rem;
|
||||
}
|
||||
.check-right { margin: 0 0 0 2rem; }
|
||||
&:hover { background-color: var(--popup-bg-hover); }
|
||||
&:hover {
|
||||
background-color: var(--popup-bg-hover);
|
||||
|
||||
.icon { color: var(--accent-color); }
|
||||
}
|
||||
}
|
||||
.sticky-wrapper {
|
||||
display: flex;
|
||||
|
@ -32,6 +32,7 @@
|
||||
export let width: string | undefined = undefined
|
||||
export let resetIconSize: boolean = false
|
||||
export let focus: boolean = false
|
||||
export let click: boolean = false
|
||||
export let title: string | undefined = undefined
|
||||
|
||||
export let input: HTMLButtonElement | undefined = undefined
|
||||
@ -43,6 +44,10 @@
|
||||
input.focus()
|
||||
focus = false
|
||||
}
|
||||
if (click && input) {
|
||||
input.click()
|
||||
click = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -128,6 +133,8 @@
|
||||
&:disabled {
|
||||
color: rgb(var(--caption-color) / 40%);
|
||||
cursor: not-allowed;
|
||||
|
||||
.btn-icon { opacity: .5; }
|
||||
}
|
||||
|
||||
&.jf-left { justify-content: flex-start; }
|
||||
|
@ -39,14 +39,14 @@
|
||||
{/if}
|
||||
{#each actions as action}
|
||||
<div
|
||||
class="ap-menuItem flex-row-center"
|
||||
class="ap-menuItem flex-row-center withIcon"
|
||||
on:click={(evt) => {
|
||||
dispatch('close')
|
||||
action.action(evt, ctx)
|
||||
}}
|
||||
>
|
||||
{#if action.icon}
|
||||
<Icon icon={action.icon} size={'small'} />
|
||||
<div class="icon"><Icon icon={action.icon} size={'small'} /></div>
|
||||
{/if}
|
||||
<div class="ml-3 pr-1"><Label label={action.label} /></div>
|
||||
</div>
|
||||
@ -55,3 +55,10 @@
|
||||
</div>
|
||||
<div class="ap-space" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.withIcon {
|
||||
.icon { color: var(--content-color); }
|
||||
&:hover .icon { color: var(--accent-color); }
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,60 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher, onMount } from 'svelte'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import contact from '@anticrm/contact'
|
||||
import { Button, Icon, IconClose, IconBlueCheck, Label } from '@anticrm/ui'
|
||||
|
||||
export let value: string = ''
|
||||
export let placeholder: IntlString
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let input: HTMLInputElement
|
||||
let phTraslate: string
|
||||
translate(placeholder, {}).then(tr => phTraslate = tr)
|
||||
|
||||
onMount(() => { if (input) input.focus() })
|
||||
</script>
|
||||
|
||||
<div class="selectPopup">
|
||||
<div class="header no-border">
|
||||
<div class="flex-between flex-grow pr-2">
|
||||
<div class="flex-grow">
|
||||
<input
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value
|
||||
placeholder={phTraslate}
|
||||
style="width: 100%;"
|
||||
on:keypress={(ev) => {
|
||||
if (ev.key === 'Enter') dispatch('close', value)
|
||||
}}
|
||||
on:change
|
||||
/>
|
||||
</div>
|
||||
<div class="buttons-group small-gap">
|
||||
<div class="clear-btn" class:show={value !== ''} on:click={() => {
|
||||
value = ''
|
||||
input.focus()
|
||||
}}>
|
||||
{#if value !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
|
||||
</div>
|
||||
<Button kind={'transparent'} size={'small'} icon={IconBlueCheck} on:click={() => dispatch('close', value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
184
plugins/contact-resources/src/components/ChannelsDropdown.svelte
Normal file
184
plugins/contact-resources/src/components/ChannelsDropdown.svelte
Normal file
@ -0,0 +1,184 @@
|
||||
<!--
|
||||
// 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 type { Channel, ChannelProvider } from '@anticrm/contact'
|
||||
import contact from '@anticrm/contact'
|
||||
import type { AttachedData, Doc, Ref, Timestamp } from '@anticrm/core'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { AnyComponent, showPopup, Tooltip, Button, Menu } from '@anticrm/ui'
|
||||
import type { Action, ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import presentation from '@anticrm/presentation'
|
||||
import { getChannelProviders } from '../utils'
|
||||
import ChannelsPopup from './ChannelsPopup.svelte'
|
||||
import ChannelEditor from './ChannelEditor.svelte'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
|
||||
export let value: AttachedData<Channel>[] | Channel | null
|
||||
export let editable = true
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let length: 'short' | 'full' = 'full'
|
||||
export let reverse: boolean = false
|
||||
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const lastViews = notificationClient.getLastViews()
|
||||
|
||||
interface Item {
|
||||
label: IntlString
|
||||
icon: Asset
|
||||
value: string
|
||||
presenter?: AnyComponent
|
||||
placeholder: IntlString
|
||||
provider: Ref<ChannelProvider>
|
||||
integration: boolean
|
||||
notification: boolean
|
||||
}
|
||||
|
||||
function getProvider (
|
||||
item: AttachedData<Channel>,
|
||||
map: Map<Ref<ChannelProvider>, ChannelProvider>,
|
||||
lastViews: Map<Ref<Doc>, Timestamp>
|
||||
): any | undefined {
|
||||
const provider = map.get(item.provider)
|
||||
if (provider) {
|
||||
const notification = (item as Channel)._id !== undefined ? isNew((item as Channel), lastViews) : false
|
||||
return {
|
||||
label: provider.label,
|
||||
icon: provider.icon as Asset,
|
||||
value: item.value,
|
||||
presenter: provider.presenter,
|
||||
placeholder: provider.placeholder,
|
||||
provider: provider._id,
|
||||
notification,
|
||||
integration: provider.integrationType !== undefined ? integrations.has(provider.integrationType) : false
|
||||
}
|
||||
} else {
|
||||
console.log('provider not found: ', item.provider)
|
||||
}
|
||||
}
|
||||
|
||||
function isNew (item: Channel, lastViews: Map<Ref<Doc>, Timestamp>): boolean {
|
||||
const lastView = (item as Channel)._id !== undefined ? lastViews.get((item as Channel)._id) : undefined
|
||||
return lastView ? lastView < item.modifiedOn : (item.items ?? 0) > 0
|
||||
}
|
||||
|
||||
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) {
|
||||
if (value === null) {
|
||||
displayItems = []
|
||||
return
|
||||
}
|
||||
const result = []
|
||||
const map = await getChannelProviders()
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const provider = getProvider(item, map, lastViews)
|
||||
if (provider !== undefined) {
|
||||
result.push(provider)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const provider = getProvider(value, map, lastViews)
|
||||
if (provider !== undefined) {
|
||||
result.push(provider)
|
||||
}
|
||||
}
|
||||
displayItems = result
|
||||
updateMenu()
|
||||
}
|
||||
|
||||
$: if (value) update(value, $lastViews)
|
||||
|
||||
let providers: Map<Ref<ChannelProvider>, ChannelProvider>
|
||||
let displayItems: Item[] = []
|
||||
let actions: Action[] = []
|
||||
let addBtn: HTMLButtonElement
|
||||
|
||||
function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] {
|
||||
return channels.filter((channel) => channel.value !== undefined && channel.value.length > 0)
|
||||
}
|
||||
|
||||
getChannelProviders().then(pr => providers = pr)
|
||||
|
||||
const updateMenu = (): void => {
|
||||
actions = []
|
||||
providers.forEach(pr => {
|
||||
if (displayItems.filter(it => it.provider === pr._id).length == 0) {
|
||||
actions.push({
|
||||
icon: pr.icon ?? contact.icon.SocialEdit,
|
||||
label: pr.label,
|
||||
action: async () => {
|
||||
const provider = getProvider({ provider: pr._id, value: '' }, providers, $lastViews)
|
||||
if (provider !== undefined) {
|
||||
if (displayItems.filter(it => it.provider === pr._id).length === 0) {
|
||||
displayItems = [...displayItems, provider]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
$: if (providers) updateMenu()
|
||||
|
||||
const editChannel = (channel: Item, n: number, ev: MouseEvent): void => {
|
||||
showPopup(
|
||||
ChannelEditor,
|
||||
{ value: channel.value, placeholder: channel.placeholder },
|
||||
ev.target as HTMLElement,
|
||||
result => {
|
||||
if (result !== undefined) {
|
||||
if (result == null || result === '') {
|
||||
displayItems = displayItems.filter((it, i) => i !== n)
|
||||
} else {
|
||||
displayItems[n].value = result
|
||||
value = filterUndefined(displayItems)
|
||||
}
|
||||
updateMenu()
|
||||
if (actions.length > 0 && addBtn) addBtn.click()
|
||||
}
|
||||
value = filterUndefined(displayItems)
|
||||
}
|
||||
)
|
||||
}
|
||||
const showMenu = (ev: MouseEvent): void => {
|
||||
showPopup(Menu, { actions }, ev.target as HTMLElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each displayItems as item, i}
|
||||
{#if item.value === ''}
|
||||
<Button
|
||||
icon={item.icon} {kind} {size} click={item.value === ''}
|
||||
on:click={(ev) => { if (editable) editChannel(item, i, ev) }}
|
||||
/>
|
||||
{:else}
|
||||
<Tooltip component={ChannelsPopup} props={{ value: item }} label={undefined}>
|
||||
<Button
|
||||
icon={item.icon} {kind} {size} click={item.value === ''}
|
||||
on:click={(ev) => { if (editable) editChannel(item, i, ev) }}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if actions.length > 0}
|
||||
<Button
|
||||
bind:input={addBtn}
|
||||
icon={contact.icon.SocialEdit}
|
||||
label={presentation.string.AddSocialLinks}
|
||||
{kind} {size}
|
||||
on:click={showMenu}
|
||||
/>
|
||||
{/if}
|
@ -22,6 +22,7 @@
|
||||
import { Button, EditBox, eventToHTMLElement, IconInfo, Label, showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
import { ChannelsDropdown } from '..'
|
||||
import ChannelsView from './ChannelsView.svelte'
|
||||
import PersonPresenter from './PersonPresenter.svelte'
|
||||
|
||||
@ -102,29 +103,18 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-4">
|
||||
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} on:remove={removeAvatar} />
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="flex-grow flex-col">
|
||||
<EditBox placeholder={contact.string.PersonFirstNamePlaceholder} bind:value={firstName} kind={'large-style'} maxWidth={'32rem'} focus />
|
||||
<EditBox placeholder={contact.string.PersonLastNamePlaceholder} bind:value={lastName} kind={'large-style'} maxWidth={'32rem'} />
|
||||
<div class="mt-1">
|
||||
<EditBox placeholder={contact.string.PersonLocationPlaceholder} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} on:remove={removeAvatar} />
|
||||
</div>
|
||||
</div>
|
||||
{#if channels.length > 0}
|
||||
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div>
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
icon={contact.icon.SocialEdit}
|
||||
kind={'transparent'}
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, eventToHTMLElement(ev), (result) => {
|
||||
if (result !== undefined) channels = result
|
||||
})
|
||||
}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<ChannelsDropdown bind:value={channels} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -22,6 +22,7 @@ import Channels from './components/Channels.svelte'
|
||||
import ChannelsEditor from './components/ChannelsEditor.svelte'
|
||||
import ChannelsPresenter from './components/ChannelsPresenter.svelte'
|
||||
import ChannelsView from './components/ChannelsView.svelte'
|
||||
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
|
||||
import ContactPresenter from './components/ContactPresenter.svelte'
|
||||
import Contacts from './components/Contacts.svelte'
|
||||
import CreateOrganization from './components/CreateOrganization.svelte'
|
||||
@ -38,7 +39,7 @@ import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svel
|
||||
import OrganizationEditor from './components/OrganizationEditor.svelte'
|
||||
import OrganizationSelector from './components/OrganizationSelector.svelte'
|
||||
|
||||
export { Channels, ChannelsEditor, ContactPresenter, ChannelsView, OrganizationSelector }
|
||||
export { Channels, ChannelsEditor, ContactPresenter, ChannelsView, OrganizationSelector, ChannelsDropdown }
|
||||
|
||||
async function queryContact (_class: Ref<Class<Contact>>, client: Client, search: string): Promise<ObjectSearchResult[]> {
|
||||
return (await client.findAll(_class, { name: { $like: `%${search}%` } }, { limit: 200 })).map(e => ({
|
||||
|
@ -151,29 +151,6 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
border-radius: 50%;
|
||||
|
||||
.icon {
|
||||
width: .625rem;
|
||||
height: .625rem;
|
||||
}
|
||||
|
||||
&.show {
|
||||
color: var(--content-color);
|
||||
background-color: var(--button-border-color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-border-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
.counter {
|
||||
padding-right: .125rem;
|
||||
min-width: 1.5rem;
|
||||
|
Loading…
Reference in New Issue
Block a user