Update Panel layout. (#1591)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-04-29 11:29:26 +03:00 committed by GitHub
parent 21f0928e71
commit c4267cc7ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 767 additions and 531 deletions

View File

@ -19,49 +19,108 @@
import type { Doc } from '@anticrm/core'
import notification from '@anticrm/notification'
import type { Asset } from '@anticrm/platform'
import { AnyComponent, AnySvelteComponent, Component, Panel } from '@anticrm/ui'
import { AnyComponent, AnySvelteComponent, Component, Panel, Icon } from '@anticrm/ui'
export let title: string | undefined = undefined
export let subtitle: string | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let showHeader: boolean = true
export let rightSection: AnyComponent | undefined = undefined
export let object: Doc
export let panelWidth: number = 0
export let innerWidth: number = 0
export let isSubtitle: boolean = false
export let isProperties: boolean = false
export let isHeader: boolean = true
export let isSub: boolean = true
export let isAside: boolean = true
export let minimize: boolean = false
let docWidth: number = 0
$: minimize = docWidth < 1280 && docWidth >= 1024
$: needHeader = $$slots.header || minimize || isHeader
</script>
<svelte:window bind:innerWidth={docWidth} />
<Panel
{title}
{subtitle}
{icon}
rightSection={rightSection !== undefined}
{showHeader}
bind:isAside
isHeader={needHeader}
bind:panelWidth
bind:innerWidth
isProperties={innerWidth >= 500 || isProperties}
isSubtitle={innerWidth < 900 || isSubtitle}
on:close
>
<svelte:fragment slot="subtitle">
{#if $$slots.subtitle}<slot name="subtitle" />{/if}
</svelte:fragment>
<svelte:fragment slot="properties">
{#if $$slots.properties}<slot name="properties" />{/if}
</svelte:fragment>
<svelte:fragment slot="navigate-actions">
<slot name="navigate-actions" />
</svelte:fragment>
<svelte:fragment slot="commands">
<div class="buttons-group xsmall-gap">
<Component is={calendar.component.DocReminder} props={{ value: object, title }} />
<Component is={notification.component.LastViewEditor} props={{ value: object }} />
<svelte:fragment slot="title">
<div class="popupPanel-title__content-container antiTitle">
{#if $$slots.navigator}
<div class="buttons-group xsmall-gap mr-4">
<slot name="navigator" />
</div>
{/if}
{#if $$slots.title}
<slot name="title" />
{:else}
<div class="icon-wrapper">
{#if icon}<div class="wrapped-icon"><Icon {icon} size={'medium'} /></div>{/if}
<div class="title-wrapper">
{#if title}<span class="wrapped-title">{title}</span>{/if}
{#if subtitle}<span class="wrapped-subtitle">{subtitle}</span>{/if}
</div>
</div>
{/if}
</div>
<slot name="actions" />
</svelte:fragment>
<Component is={rightSection ?? activity.component.Activity} props={{ object, integrate: true }}>
<svelte:fragment slot="utils">
<Component is={calendar.component.DocReminder} props={{ value: object, title }} />
<Component is={notification.component.LastViewEditor} props={{ value: object }} />
{#if $$slots.utils}
<div class="buttons-divider" />
<slot name="utils" />
{/if}
</svelte:fragment>
<svelte:fragment slot="header">
{#if $$slots.header || ($$slots.actions && minimize)}
<div class="header-row between">
{#if $$slots.header}<slot name="header" />{/if}
<div class="buttons-group xsmall-gap ml-4">
<slot name="tools" />
{#if $$slots.actions && minimize}
<div class="buttons-divider" />
<slot name="actions" />
{/if}
</div>
</div>
{/if}
{#if $$slots['custom-attributes']}
{#if isSub}<div class="header-row"><slot name="custom-attributes" direction="row" /></div>{/if}
{:else}
{#if $$slots.attributes && minimize}<div class="header-row"><slot name="attributes" direction="row" /></div>{/if}
{/if}
</svelte:fragment>
<svelte:fragment slot="aside">
<div style="padding: .75rem 1.5rem">
{#if $$slots.actions}
<div class="flex-row-center pb-3 bottom-divider">
<span class="fs-bold w-24 mr-6"><slot name="actions-label" /></span>
<div class="buttons-group xsmall-gap">
<slot name="actions" />
</div>
</div>
{/if}
{#if $$slots['custom-attributes']}
<slot name="custom-attributes" direction="column" />
{:else}
{#if $$slots.attributes}<slot name="attributes" direction="column" />{/if}
{/if}
{#if $$slots.aside}<slot name="aside" />{/if}
</div>
</svelte:fragment>
{#if rightSection !== undefined}
<slot />
</Component>
{:else}
<Component is={activity.component.Activity} props={{ object, integrate: true }}>
<slot />
</Component>
{/if}
</Panel>

View File

@ -80,6 +80,8 @@
--popup-bg-hover: #37373c;
--popup-divider: #313236;
--popup-shadow: rgb(0 0 0 / 20%) 0px 4px 24px;
--popup-panel-shadow: rgb(0 0 0 / 55%) 0px 7px 24px;
--popup-aside-shadow: rgb(0 0 0 / 25%) 0px 8px 16px;
--card-shadow: rgb(0 0 0 / 50%) 0px 16px 70px;
--card-overlay-color: rgba(28, 29, 31, .5);
--avatar-bg-color: #4f5358;
@ -220,6 +222,7 @@
--popup-bg-hover: #f8f9fb;
--popup-divider: #eff1f4;
--popup-shadow: rgb(0 0 0 / 20%) 0px 4px 24px; // Dark
--popup-panel-shadow: rgb(0 0 0 / 55%) 0px 7px 24px; // Dark
--card-shadow: rgb(0 0 0 / 50%) 0px 16px 70px;
--card-overlay-color: rgba(144, 149, 157, .4);
--avatar-bg-color: #e0e0e0; // HZ

View File

@ -263,6 +263,12 @@ p:last-child { margin-block-end: 0; }
grid-auto-flow: row;
}
}
.buttons-divider {
min-width: 1px;
width: 1px;
height: 1.5rem;
background-color: var(--divider-color);
}
.gap-1, .gap-1-5, .gap-2 {
& > *:not(:last-child) { margin-right: .25rem; }
@ -365,6 +371,7 @@ p:last-child { margin-block-end: 0; }
.pb-3 { padding-bottom: .75rem; }
.pb-4 { padding-bottom: 1rem; }
.px-2 { padding: 0 .5rem; }
.px-3 { padding: 0 .75rem; }
.px-4 { padding: 0 1rem; }
.p-1 { padding: .25rem; }
@ -419,6 +426,7 @@ p:last-child { margin-block-end: 0; }
.h-7 { height: 1.75rem; }
.h-8 { height: 2rem; }
.h-9 { height: 2.25rem; }
.h-14 { height: 3.5rem; }
.h-16 { height: 4rem; }
.h-18 { height: 4.5rem; }
.w-full { width: 100%; }

View File

@ -202,3 +202,48 @@
.antiNav-bottomFade { mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1px, rgba(0, 0, 0, 1) calc(100% - 2rem), rgba(0, 0, 0, 0) 100%); }
.antiNav-bothFade { mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 2rem, rgba(0, 0, 0, 1) calc(100% - 2rem), rgba(0, 0, 0, 0) 100%); }
.antiNav-noneFade { mask-image: linear-gradient(0deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1px, rgba(0, 0, 0, 1) calc(100% - 1px), rgba(0, 0, 0, 0) 100%); }
/* Basic */
.antiTitle {
.icon-wrapper,
.title-wrapper {
display: flex;
flex-wrap: nowrap;
min-width: 0;
}
.title-wrapper {
flex-direction: column;
flex-grow: 1;
}
.icon-wrapper { align-items: center; }
.wrapped-icon {
margin-right: .75rem;
color: var(--content-color);
}
.wrapped-title {
min-width: 0;
font-weight: 500;
font-size: 1rem;
color: var(--caption-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
}
.wrapped-subtitle {
min-width: 0;
font-size: 0.75rem;
color: var(--dark-color);
overflow: hidden;
visibility: visible;
display: -webkit-box;
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
user-select: none;
}
}

View File

@ -55,3 +55,159 @@
}
}
.popupPanel {
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
border-radius: .5rem;
box-shadow: var(--popup-panel-shadow);
.popupPanel-title {
display: flex;
justify-content: stretch;
align-items: center;
flex-shrink: 0;
padding: .5rem .75rem;
width: 100%;
height: 3rem;
max-height: 3rem;
background-color: var(--board-card-bg-color);
border: 1px solid var(--divider-color);
border-bottom: none;
border-radius: .5rem .5rem 0 0;
&__content {
flex-grow: 1;
margin: 0 .75rem;
min-width: 0;
min-height: 0;
&-container {
display: flex;
justify-content: stretch;
min-width: 0;
width: 100%;
}
}
}
.popupPanel-body {
position: relative;
display: flex;
justify-content: stretch;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
background-color: var(--body-color);
border: 1px solid var(--divider-color);
border-radius: 0 0 .5rem .5rem;
&__main, &__aside {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
height: 100%;
}
&__main {
flex-grow: 2;
flex-basis: 760px;
border: none;
border-radius: 0;
&-header {
display: flex;
flex-direction: column;
margin: 0 auto;
padding: .75rem 1.25rem;
width: calc(100% - 5rem);
min-width: 0;
max-width: 900px;
border-bottom: 1px solid var(--divider-color);
.header-row {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
&.between { justify-content: space-between; }
}
.header-row + .header-row { margin-top: .625rem; }
}
&-content {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 0;
margin-left: auto;
margin-right: auto;
width: calc(100% - 7.5rem);
height: 100%;
max-width: 860px;
}
}
&__aside {
position: relative;
width: 25%;
min-width: 320px;
&::before {
position: absolute;
content: '';
top: 1rem;
bottom: 1rem;
left: 0;
width: 0;
border-left: 1px solid var(--divider-color);
}
&.float {
position: absolute;
flex-shrink: 0;
top: 0;
left: 100%;
width: 320px;
height: 100%;
min-width: 0;
background-color: var(--board-card-bg-color);
border-top: 1px solid var(--board-card-bg-color);
border-left: 1px solid var(--divider-color);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
transition: box-shadow 150ms ease 0s, transform 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform: translateX(0);
&::before { content: none; }
&.shown {
box-shadow: var(--popup-aside-shadow);
transform: translateX(-100%);
&::after {
position: absolute;
content: '';
top: 0;
left: 100%;
width: 360px;
height: 100%;
background-color: var(--board-card-bg-color);
border: 1px solid var(--board-card-bg-color);
}
}
}
}
&.asideShown {
border: none;
.popupPanel-body__main {
border: 1px solid var(--divider-color);
border-radius: 0 0 .5rem .5rem;
}
}
}
}

View File

@ -32,6 +32,7 @@
export let width: string | undefined = undefined
export let resetIconSize: boolean = false
export let highlight: boolean = false
export let selected: boolean = false
export let focus: boolean = false
export let click: boolean = false
export let title: string | undefined = undefined
@ -60,6 +61,7 @@
class:border-radius-2={shape === 'round'}
class:border-radius-4={shape === 'circle'}
class:highlight
class:selected
disabled={disabled || loading}
style={width ? 'width: ' + width : ''}
{title}
@ -215,9 +217,8 @@
}
}
}
&.transparent:hover {
background-color: var(--button-bg-hover);
}
&.transparent:hover,
&.transparent.selected { background-color: var(--button-bg-hover); }
&.link {
padding: 0 0.875rem;
&:hover {

View File

@ -13,152 +13,65 @@
// limitations under the License.
-->
<script lang="ts">
import type { Asset } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte'
import { AnySvelteComponent } from '../types'
import Button from './Button.svelte'
import Icon from './Icon.svelte'
import IconClose from './icons/Close.svelte'
import { Button, IconClose, IconDetails, Scroller } from '..'
export let title: string | undefined = undefined
export let subtitle: string | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let rightSection: boolean = false
export let showHeader: boolean = true
export let innerWidth: number = 0
export let panelWidth: number = 0
export let isSubtitle: boolean = true
export let isProperties: boolean = true
export let isHeader: boolean = true
export let isAside: boolean = true
const dispatch = createEventDispatcher()
let asideFloat: boolean = false
let asideShown: boolean = false
let docWidth: number
$: if (docWidth < 1024 && !asideFloat) asideFloat = true
$: if (docWidth >= 1024 && asideFloat) {
asideFloat = false
asideShown = false
}
</script>
<div class="antiPanel antiComponent" bind:clientWidth={panelWidth}>
<div class="panel-content-container">
{#if showHeader}
<div class="ac-header short mirror divide highlight">
<div class="buttons-group">
<Button
icon={IconClose}
size={'medium'}
kind={'transparent'}
on:click={() => {
dispatch('close')
}}
/>
{#if $$slots['navigate-actions']}
<div class="buttons-group xsmall-gap">
<slot name="navigate-actions" />
</div>
{/if}
</div>
<div class="ml-4 ac-header__wrap-title flex-grow">
{#if icon}
<div class="ac-header__icon">
<Icon {icon} size={'large'} />
</div>
{/if}
<div class="ac-header__wrap-description">
{#if title}<span class="ac-header__title">{title}</span>{/if}
{#if subtitle}<span class="ac-header__description">{subtitle}</span>{/if}
</div>
</div>
<div class="buttons-group xsmall-gap">
<slot name="commands" />
<slot name="actions" />
</div>
</div>
{:else}
<div class="ac-header short mirror divide highlight">
<div class="buttons-group">
<Button
icon={IconClose}
size={'medium'}
kind={'transparent'}
on:click={() => {
dispatch('close')
}}
/>
{#if $$slots['navigate-actions']}
<div class="buttons-group xsmall-gap">
<slot name="navigate-actions" />
</div>
{/if}
</div>
{#if $$slots['custom-title']}
<div class="ml-4 flex-row-center flex-grow">
<slot name="custom-title" />
</div>
{/if}
</div>
{/if}
<div class="main-content" class:withProperties={$$slots.properties} bind:clientWidth={innerWidth}>
{#if $$slots.subtitle && $$slots.properties && isSubtitle}
<div class="flex-col flex-grow clear-mins">
<div class="ac-subtitle">
<div class="ac-subtitle-content">
<slot name="subtitle" />
</div>
</div>
<div class="flex-col flex-grow clear-mins">
<slot />
</div>
</div>
{:else}
<div class="flex-col flex-grow clear-mins">
<slot />
</div>
{/if}
{#if rightSection}
<slot name="rightSection" />
{/if}
{#if $$slots.properties && isProperties}
<div class="properties-container">
<slot name="properties" />
</div>
<svelte:window bind:innerWidth={docWidth} />
<div class="popupPanel" bind:clientWidth={panelWidth}>
<div class="popupPanel-title">
<Button icon={IconClose} kind={'transparent'} size={'medium'} on:click={() => { dispatch('close') }} />
<div class="popupPanel-title__content"><slot name="title" /></div>
<div class="buttons-group xsmall-gap">
<slot name="utils" />
{#if asideFloat}
{#if $$slots.utils}<div class="buttons-divider" />{/if}
<Button
icon={IconDetails}
kind={'transparent'}
size={'medium'}
selected={asideShown}
on:click={() => {
asideShown = !asideShown
}}
/>
{/if}
</div>
</div>
<div class="popupPanel-body" class:asideShown>
<div class="popupPanel-body__main" bind:clientWidth={innerWidth}>
{#if $$slots.header && isHeader}
<div class="popupPanel-body__main-header">
<slot name="header" />
</div>
{/if}
<Scroller>
<div class="popupPanel-body__main-content">
<slot />
</div>
</Scroller>
</div>
{#if $$slots.aside && isAside}
<div class="popupPanel-body__aside" class:float={asideFloat} class:shown={asideShown}>
<slot name="aside" />
</div>
{/if}
</div>
</div>
<style lang="scss">
.panel-content-container {
display: flex;
flex-grow: 1;
flex-direction: column;
}
.main-content {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-width: 0;
min-height: 0;
flex-grow: 1;
// height: 100%;
&.withProperties {
flex-direction: row;
}
.properties-container {
position: relative;
display: flex;
flex-direction: column;
flex-shrink: 0;
min-width: 20rem;
width: 20rem;
border-left: 1px solid var(--divider-color);
// background-color: var(--board-card-bg-color);
// &::before {
// position: absolute;
// content: '';
// top: 1.5rem;
// bottom: 1.5rem;
// left: 0;
// width: 1px;
// background-color: var(--divider-color);
// }
}
}
</style>

View File

@ -0,0 +1,25 @@
<!--
// 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: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M 4.25 2 C 2.45508 2 1 3.45508 1 5.25 V 10.7499 C 1 12.5449 2.45508 13.9999 4.25 13.9999 H 11.75 C 13.5449 13.9999 15 12.5449 15 10.7499 V 5.25 C 15 3.45508 13.5449 2 11.75 2 H 4.25 Z M 2.5 10.4999 C 2.5 11.6045 3.39543 12.4999 4.5 12.4999 H 11.75 C 12.7165 12.4999 13.5 11.7164 13.5 10.7499 V 5.25 C 13.5 4.28351 12.7165 3.5 11.75 3.5 H 4.5 C 3.39543 3.5 2.5 4.39543 2.5 5.5 V 10.4999 Z" />
<rect x="9" y="3" width="1.5" height="10" />
</svg>

View File

@ -119,6 +119,7 @@ export { default as IconNavNext } from './components/icons/NavNext.svelte'
export { default as IconDPCalendar } from './components/calendar/icons/DPCalendar.svelte'
export { default as IconDPCalendarOver } from './components/calendar/icons/DPCalendarOver.svelte'
export { default as IconOptions } from './components/icons/Options.svelte'
export { default as IconDetails } from './components/icons/Details.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte'

View File

@ -195,6 +195,13 @@ export function fitPopupElement (modalHTML: HTMLElement, element?: PopupAlignmen
newProps.left = '50%'
newProps.transform = 'translateX(-50%)'
show = true
} else if (element === 'float') {
newProps.top = 'calc(var(--status-bar-height) + .25rem)'
newProps.bottom = '.25rem'
newProps.width = '40rem'
newProps.maxWidth = '40%'
newProps.right = '.25rem'
show = true
} else if (element === 'account') {
newProps.bottom = '2.75rem'
newProps.left = '5rem'

View File

@ -72,7 +72,7 @@ export interface PopupPositionElement {
h: HorizontalAlignment
}
}
export type PopupAlignment = PopupPositionElement | null | 'right' | 'top' | 'account' | 'full' | 'content' | 'middle'
export type PopupAlignment = PopupPositionElement | null | 'right' | 'top' | 'float' | 'account' | 'full' | 'content' | 'middle'
export type TooltipAlignment = 'top' | 'bottom' | 'left' | 'right'
export type VerticalAlignment = 'top' | 'bottom'
export type HorizontalAlignment = 'left' | 'right'

View File

@ -82,31 +82,29 @@
{/if}
</div>
{:else}
<Scroller>
<div class="p-10 bottom-highlight-select">
<slot />
<div class="mt-4 pb-4 bottom-highlight-select">
<slot />
</div>
<div class="flex-row-center h-14 px-3 mt-4 antiTitle">
<div class="icon-wrapper">
<div class="wrapped-icon icon flex-center"><IconActivity size={'small'} /></div>
<span class="wrapped-title"><Label label={activity.string.Activity} /></span>
</div>
<div class="ac-header short mirror-tool mt-2">
<div class="ac-header__wrap-title">
<div class="flex-center icon"><IconActivity size={'small'} /></div>
<span class="ac-header__title"><Label label={activity.string.Activity} /></span>
</div>
</div>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{/if}
<div class="p-activity">
{#if txes}
<Grid column={1} rowGap={1.5}>
{#each txes as tx}
<TxView {tx} {viewlets} />
{/each}
</Grid>
{/if}
<div class="p-activity">
{#if txes}
<Grid column={1} rowGap={1.5}>
{#each txes as tx}
<TxView {tx} {viewlets} />
{/each}
</Grid>
{/if}
</div>
</Scroller>
</div>
{/if}
<style lang="scss">
@ -120,10 +118,11 @@
}
.ref-input {
flex-shrink: 0;
padding: 1.5rem 2.5rem;
padding: 1.5rem 0;
}
.p-activity {
padding: 1.5rem 2.5rem;
padding: 1.5rem 0;
}
:global(.grid .msgactivity-container:last-child::after) {

View File

@ -22,7 +22,7 @@
import type { State } from '@anticrm/task'
import task from '@anticrm/task'
import { StyledTextBox } from '@anticrm/text-editor'
import { EditBox, Icon, Label } from '@anticrm/ui'
import { Button, EditBox, Icon, Label } from '@anticrm/ui'
import { UpDownNavigator } from '@anticrm/view-resources'
import { createEventDispatcher, onMount } from 'svelte'
import board from '../plugin'
@ -72,12 +72,21 @@
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'number', 'title'] })
})
let minimize: boolean = false
</script>
{#if object !== undefined}
<Panel icon={board.icon.Card} title={object?.title} {object} on:close={() => dispatch('close')}>
<svelte:fragment slot="navigate-actions">
<UpDownNavigator element={object} />
<Panel
icon={board.icon.Card}
title={object?.title}
{object}
bind:minimize
isHeader={minimize}
isAside={!minimize}
on:close={() => dispatch('close')}
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={object}/>
</svelte:fragment>
<!-- TODO cover -->
@ -128,8 +137,12 @@
</div>
</div>
<svelte:fragment slot="properties">
<div class="p-4"><CardActions bind:value={object} /></div>
<svelte:fragment slot="custom-attributes" let:direction>
{#if direction === 'column'}
<CardActions bind:value={object} />
{:else}
<Button icon={board.icon.Card} label={board.string.Actions} kind={'no-border'} size={'small'} />
{/if}
</svelte:fragment>
</Panel>
{/if}

View File

@ -18,7 +18,6 @@
import type { ButtonKind, ButtonSize } from '@anticrm/ui'
import ChannelsDropdown from './ChannelsDropdown.svelte'
import { showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
export let value: Channel[] | Channel | null
@ -32,7 +31,13 @@
if (ev.detail.presenter !== undefined && Array.isArray(value)) {
const channel = value[0]
if (channel !== undefined) {
showPanel(view.component.EditDoc, channel.attachedTo, channel.attachedToClass, 'content', ev.detail.presenter)
showPanel(
ev.detail.presenter,
channel.attachedTo,
channel.attachedToClass,
'float',
ev.detail.presenter
)
}
}
}

View File

@ -45,6 +45,7 @@
"@anticrm/attachment": "~0.6.1",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/login": "~0.6.1",
"@anticrm/core": "~0.6.16"
"@anticrm/core": "~0.6.16",
"@anticrm/panel": "~0.6.0"
}
}

View File

@ -195,6 +195,7 @@
border-bottom: 1px solid var(--theme-zone-bg);
.icon {
flex-shrink: 0;
margin-right: 1rem;
width: 2.25rem;
height: 2.25rem;
@ -206,6 +207,6 @@
.right-content {
flex-grow: 1;
padding: 1.5rem 1rem;
padding: 1.5rem 0;
}
</style>

View File

@ -14,31 +14,49 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Channel, Contact } from '@anticrm/contact'
import type { AnyComponent } from '@anticrm/ui'
import { Ref, Doc, Class } from '@anticrm/core'
import contact, { Channel } from '@anticrm/contact'
import { SharedMessage } from '@anticrm/gmail'
import NewMessage from './NewMessage.svelte'
import FullMessage from './FullMessage.svelte'
import Chats from './Chats.svelte'
import { getClient } from '@anticrm/presentation'
import { createQuery, getClient } from '@anticrm/presentation'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { Panel } from '@anticrm/panel'
import { createEventDispatcher } from 'svelte'
export let object: Contact
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
export let rightSection: AnyComponent | undefined = undefined
// export let object: Contact
// $: console.log('!!!!!!!!!!!! id: ', _id, ' - class: ', _class)
let object: any
let newMessage: boolean = false
let currentMessage: SharedMessage | undefined = undefined
let channel: Channel | undefined = undefined
const notificationClient = NotificationClientImpl.getClient()
const client = getClient()
const dispatch = createEventDispatcher()
client
.findOne(contact.class.Channel, {
attachedTo: object._id,
attachedTo: _id,
provider: contact.channelProvider.Email
})
.then((res) => {
channel = res
})
const query = createQuery()
$: _id &&
_class &&
query.query(_class, { _id }, (result) => {
object = result[0]
})
function back () {
if (newMessage) {
return (newMessage = false)
@ -54,12 +72,24 @@
}
</script>
{#if channel}
{#if newMessage}
<NewMessage {object} {channel} {currentMessage} on:close={back} />
{:else if currentMessage}
<FullMessage {currentMessage} bind:newMessage on:close={back} />
{:else}
<Chats {object} {channel} bind:newMessage on:select={selectHandler} />
{/if}
{#if channel && object}
<Panel
icon={contact.icon.Email}
title={'Email'}
{rightSection}
{object}
isHeader={false}
isAside={false}
on:close={() => {
dispatch('close')
}}
>
{#if newMessage}
<NewMessage {object} {channel} {currentMessage} on:close={back} />
{:else if currentMessage}
<FullMessage {currentMessage} bind:newMessage on:close={back} />
{:else}
<Chats {object} {channel} bind:newMessage on:select={selectHandler} />
{/if}
</Panel>
{/if}

View File

@ -43,6 +43,7 @@
"@anticrm/core": "~0.6.16",
"@anticrm/notification-resources": "~0.6.0",
"@anticrm/attachment": "~0.6.1",
"@anticrm/attachment-resources": "~0.6.0"
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/panel": "~0.6.0"
}
}

View File

@ -16,12 +16,15 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import contact, { Channel, Contact, EmployeeAccount, formatName } from '@anticrm/contact'
import { generateId, getCurrentAccount, Ref, SortingOrder, Space } from '@anticrm/core'
import { Panel } from '@anticrm/panel'
import { createEventDispatcher } from 'svelte'
import contact, { Channel, EmployeeAccount, formatName } from '@anticrm/contact'
import { generateId, getCurrentAccount, Ref, SortingOrder, Space, Doc, Class } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { createQuery, getClient } from '@anticrm/presentation'
import setting, { Integration } from '@anticrm/setting'
import type { NewTelegramMessage, SharedTelegramMessage, TelegramMessage } from '@anticrm/telegram'
import type { AnyComponent } from '@anticrm/ui'
import { Button, eventToHTMLElement, IconShare, Tooltip, Scroller, showPopup } from '@anticrm/ui'
import telegram from '../plugin'
import Connect from './Connect.svelte'
@ -29,22 +32,35 @@
import Messages from './Messages.svelte'
import Reconnect from './Reconnect.svelte'
export let object: Contact
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
export let rightSection: AnyComponent | undefined = undefined
let object: any
let channel: Channel | undefined = undefined
let objectId: Ref<NewTelegramMessage> = generateId()
const dispatch = createEventDispatcher()
const client = getClient()
const notificationClient = NotificationClientImpl.getClient()
client
.findOne(contact.class.Channel, {
attachedTo: object._id,
attachedTo: _id,
provider: contact.channelProvider.Telegram
})
.then((res) => {
channel = res
})
const query = createQuery()
$: _id &&
_class &&
query.query(_class, { _id }, (result) => {
object = result[0]
})
let messages: TelegramMessage[] = []
let accounts: EmployeeAccount[] = []
let integration: Integration | undefined
@ -171,111 +187,101 @@
}
</script>
<div class="telegram-header">
<div class="ac-header__wrap-title">
<div class="flex-center icon"><TelegramIcon size={'small'} /></div>
<div class="ac-header__wrap-description">
<span class="ac-header__title">Telegram</span>
<span class="ac-header__description">You and {formatName(object.name)}</span>
</div>
</div>
<Tooltip label={telegram.string.Share}>
<Button
icon={IconShare}
kind={'transparent'}
size={'medium'}
on:click={async () => {
selectable = !selectable
}}
/>
</Tooltip>
</div>
<div class="telegram-content" class:selectable>
<Scroller bottomStart autoscroll>
{#if messages && accounts}
<Messages messages={convertMessages(messages, accounts)} {selectable} bind:selected />
{/if}
</Scroller>
</div>
{#if object !== undefined}
<Panel
icon={TelegramIcon}
title={'Telegram'}
{rightSection}
{object}
isHeader={false}
isAside={false}
on:close={() => {
dispatch('close')
}}
>
<svelte:fragment slot="header">
You and {formatName(object.name)}
</svelte:fragment>
<svelte:fragment slot="tools">
<Tooltip label={telegram.string.Share}>
<Button
icon={IconShare}
kind={'transparent'}
size={'medium'}
on:click={async () => {
selectable = !selectable
}}
/>
</Tooltip>
</svelte:fragment>
<div class="ref-input" class:selectable>
{#if selectable}
<div class="flex-between">
<span>{selected.size} messages selected</span>
<div class="flex">
<div>
<Button label={telegram.string.Cancel} size={'medium'} on:click={clear} />
<div class="telegram-content" class:selectable>
<Scroller bottomStart autoscroll>
{#if messages && accounts}
<Messages messages={convertMessages(messages, accounts)} {selectable} bind:selected />
{/if}
</Scroller>
</div>
<div class="ref-input" class:selectable>
{#if selectable}
<div class="flex-between">
<span>{selected.size} messages selected</span>
<div class="flex">
<div>
<Button label={telegram.string.Cancel} size={'medium'} on:click={clear} />
</div>
<div class="ml-3">
<Button
label={telegram.string.PublishSelected}
size={'medium'}
kind={'primary'}
disabled={!selected.size}
on:click={share}
/>
</div>
</div>
</div>
<div class="ml-3">
{:else if integration === undefined}
<div class="flex-center">
<Button
label={telegram.string.PublishSelected}
size={'medium'}
label={telegram.string.Connect}
kind={'primary'}
disabled={!selected.size}
on:click={share}
on:click={(e) => {
showPopup(Connect, {}, eventToHTMLElement(e), onConnectClose)
}}
/>
</div>
</div>
{:else if integration.disabled}
<div class="flex-center">
<Button
label={setting.string.Reconnect}
kind={'primary'}
on:click={(e) => {
showPopup(Reconnect, {}, eventToHTMLElement(e), onReconnect)
}}
/>
</div>
{:else}
<AttachmentRefInput
space={telegram.space.Telegram}
_class={telegram.class.NewMessage}
{objectId}
on:message={onMessage}
/>
{/if}
</div>
{:else if integration === undefined}
<div class="flex-center">
<Button
label={telegram.string.Connect}
kind={'primary'}
on:click={(e) => {
showPopup(Connect, {}, eventToHTMLElement(e), onConnectClose)
}}
/>
</div>
{:else if integration.disabled}
<div class="flex-center">
<Button
label={setting.string.Reconnect}
kind={'primary'}
on:click={(e) => {
showPopup(Reconnect, {}, eventToHTMLElement(e), onReconnect)
}}
/>
</div>
{:else}
<AttachmentRefInput
space={telegram.space.Telegram}
_class={telegram.class.NewMessage}
{objectId}
on:message={onMessage}
/>
{/if}
</div>
</Panel>
{/if}
<style lang="scss">
.telegram-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
margin: 0 2.5rem;
padding: 0;
height: 4rem;
min-height: 4rem;
border-bottom: 1px solid var(--divider-color);
}
.icon {
margin-right: 1rem;
width: 2.25rem;
height: 2.25rem;
color: var(--white-color);
background-color: var(--primary-bg-color);
border-radius: 50%;
}
.ref-input {
padding: 0 2.5rem 1.5rem;
padding: 0 0 1.5rem;
&.selectable {
padding: 1rem 2.5rem;
padding: 1rem 0;
color: var(--theme-caption-color);
background-color: var(--accent-bg-color);
border-top: 1px solid var(--theme-card-divider);
border-top: 1px solid var(--divider-color);
}
}

View File

@ -80,13 +80,16 @@
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] })
})
let minimize: boolean = false
</script>
{#if issue !== undefined}
<Panel
object={issue}
showHeader={false}
isSubtitle={true}
bind:minimize
isHeader
isAside={!minimize}
isSub={minimize}
bind:innerWidth
on:close={() => {
dispatch('close')
@ -106,115 +109,135 @@
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="navigate-actions">
<svelte:fragment slot="navigator">
<Button icon={IconDownOutline} kind={'secondary'} size={'medium'} />
<Button icon={IconUpOutline} kind={'secondary'} size={'medium'} />
</svelte:fragment>
<svelte:fragment slot="header">
<span class="fs-title">{issueLabel}</span>
</svelte:fragment>
<svelte:fragment slot="tools">
<Button icon={IconEdit} kind={'transparent'} size={'medium'} />
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} />
</svelte:fragment>
<div class="flex-col flex-grow flex-no-shrink h-full mx-auto content">
<div class="mt-6">
<EditBox
label={tracker.string.Title}
bind:value={issue.title}
placeholder={tracker.string.IssueTitlePlaceholder}
maxWidth={'16rem'}
focus
on:change={() => change('title', issue?.title)}
/>
</div>
<div class="mt-6">
<StyledTextBox
alwaysEdit
bind:content={issue.description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<div class="mt-6">
<EditBox
label={tracker.string.Title}
bind:value={issue.title}
placeholder={tracker.string.IssueTitlePlaceholder}
maxWidth={'16rem'}
focus
on:change={() => change('title', issue?.title)}
/>
</div>
<div class="mt-6 mb-6">
<StyledTextBox
alwaysEdit
bind:content={issue.description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
on:value={(evt) => change('description', evt.detail)}
/>
</div>
<svelte:fragment slot="properties">
{#if issue && currentTeam && issueStatuses}
<div class="flex-grow relative min-w-80 right-panel">
<div class="ac-header short divide header">
<span class="w-24 overflow-label">{issueLabel}</span>
<div class="buttons-group">
<Button
icon={tracker.icon.Issue}
title={tracker.string.CopyIssueUrl}
width="min-content"
size="small"
kind="transparent"
on:click={() => copy(window.location.href)}
/>
<Button
icon={tracker.icon.Views}
title={tracker.string.CopyIssueId}
width="min-content"
size="small"
kind="transparent"
on:click={() => issueLabel && copy(issueLabel)}
/>
</div>
<span slot="actions-label">{issueLabel}</span>
<svelte:fragment slot="actions">
<Button
icon={tracker.icon.Issue}
title={tracker.string.CopyIssueUrl}
width="min-content"
size="small"
kind="transparent"
on:click={() => copy(window.location.href)}
/>
<Button
icon={tracker.icon.Views}
title={tracker.string.CopyIssueId}
width="min-content"
size="small"
kind="transparent"
on:click={() => issueLabel && copy(issueLabel)}
/>
</svelte:fragment>
<svelte:fragment slot="custom-attributes" let:direction>
{#if issue && currentTeam && issueStatuses && direction === 'column'}
<div class="content mt-4">
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Status} />
</span>
<StatusPresenter value={issue} statuses={issueStatuses} currentSpace={currentTeam._id} shouldShowLabel />
</div>
<div class="content">
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Status} />
</span>
<StatusPresenter value={issue} statuses={issueStatuses} currentSpace={currentTeam._id} shouldShowLabel />
</div>
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Priority} />
</span>
<PriorityPresenter value={issue} currentSpace={currentTeam._id} shouldShowLabel />
</div>
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Assignee} />
</span>
<UserBox
_class={contact.class.Employee}
label={tracker.string.Assignee}
placeholder={tracker.string.Assignee}
bind:value={issue.assignee}
allowDeselect
titleDeselect={tracker.string.Unassigned}
on:change={() => change('assignee', issue?.assignee)}
/>
</div>
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Labels} />
</span>
<Button
label={tracker.string.Labels}
icon={tracker.icon.Labels}
width="min-content"
size="small"
kind="no-border"
/>
</div>
<div class="devider" />
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Project} />
</span>
<Button
label={tracker.string.Project}
icon={tracker.icon.Projects}
width="min-content"
size="small"
kind="no-border"
/>
</div>
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Priority} />
</span>
<PriorityPresenter value={issue} currentSpace={currentTeam._id} shouldShowLabel />
</div>
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Assignee} />
</span>
<UserBox
_class={contact.class.Employee}
label={tracker.string.Assignee}
placeholder={tracker.string.Assignee}
bind:value={issue.assignee}
allowDeselect
titleDeselect={tracker.string.Unassigned}
size="large"
kind="link"
on:change={() => change('assignee', issue?.assignee)}
/>
</div>
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Labels} />
</span>
<Button
label={tracker.string.Labels}
icon={tracker.icon.Labels}
width="max-content"
size="large"
kind="link"
/>
</div>
<div class="devider" />
<div class="flex-row-center mb-4">
<span class="label w-24">
<Label label={tracker.string.Project} />
</span>
<Button
label={tracker.string.Project}
icon={tracker.icon.Projects}
width="fit-content"
size="large"
kind="link"
/>
</div>
</div>
{:else}
<div class="buttons-group small-gap">
<Button
label={tracker.string.Labels}
icon={tracker.icon.Labels}
width="min-content"
size="small"
kind="no-border"
/>
<Button
label={tracker.string.Project}
icon={tracker.icon.Projects}
width="min-content"
size="small"
kind="no-border"
/>
</div>
{/if}
</svelte:fragment>
@ -222,37 +245,23 @@
{/if}
<style lang="scss">
.header {
max-width: 56.25rem;
width: calc(100% - 5rem);
justify-content: space-between;
padding: 0 1.25rem;
}
.content {
width: 100%;
}
.right-panel {
.header {
padding: 1rem 0;
margin: 0 1.5rem;
}
.content {
position: absolute;
inset: 2.5rem 0 0;
padding: 1.5rem 0.5rem 1.5rem 1.5rem;
.content {
position: absolute;
inset: 2.5rem 0 0;
padding: 1.5rem 0.5rem 1.5rem 1.5rem;
.label {
margin: 0.625rem 0;
}
}
.devider {
height: 1px;
border-bottom: 1px solid var(--divider-color);
margin: 0.75rem 1.5rem 1.25rem 0;
.label {
margin: 0.625rem 0;
}
}
.devider {
height: 1px;
border-bottom: 1px solid var(--divider-color);
margin: 0.75rem 1.5rem 1.25rem 0;
}
</style>

View File

@ -15,7 +15,7 @@
-->
<script lang="ts">
import contact, { ChannelProvider, formatName } from '@anticrm/contact'
import core, { Class, ClassifierKind, Doc, getCurrentAccount, Hierarchy, Mixin, Obj, Ref, Space } from '@anticrm/core'
import core, { Class, ClassifierKind, Doc, getCurrentAccount, Mixin, Obj, Ref, Space } from '@anticrm/core'
import notification from '@anticrm/notification'
import { Panel } from '@anticrm/panel'
import { Asset, getResource, IntlString, translate } from '@anticrm/platform'
@ -27,8 +27,7 @@
KeyedAttribute
} from '@anticrm/presentation'
import setting, { IntegrationType } from '@anticrm/setting'
import { AnyComponent, Button, Component, IconActivity } from '@anticrm/ui'
import Tooltip from '@anticrm/ui/src/components/Tooltip.svelte'
import { AnyComponent, Component } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher, onDestroy } from 'svelte'
import { getCollectionCounter } from '../utils'
@ -38,7 +37,6 @@
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
export let rightSection: AnyComponent | undefined = undefined
// export let position: PopupAlignment | undefined = undefined
let lastId: Ref<Doc> = _id
let lastClass: Ref<Class<Doc>> = _class
@ -49,7 +47,6 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys())
$: read(_id)
@ -76,18 +73,11 @@
$: if (object !== undefined) objectClass = hierarchy.getClass(object._class)
let selectedClass: Ref<Class<Doc>> | undefined
let prevSelected = selectedClass
let keys: KeyedAttribute[] = []
let collectionEditors: { key: KeyedAttribute; editor: AnyComponent }[] = []
let mixins: Mixin<Doc>[] = []
$: if (object && prevSelected !== object._class) {
prevSelected = object._class
selectedClass = Hierarchy.mixinOrClass(object)
$: if (object) {
parentClass = getParentClass(object._class)
getMixins()
}
@ -119,7 +109,11 @@
let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
async function updateKeys (): Promise<void> {
const filtredKeys = getFiltredKeys(selectedClass ?? object._class, ignoreKeys)
let filtredKeys = getFiltredKeys(object._class, ignoreKeys)
for (const m of mixins) {
const mkeys = getFiltredKeys(m._id, ignoreKeys)
filtredKeys = filtredKeys.concat(mkeys).filter((it, idx, arr) => arr.indexOf(it) === idx)
}
keys = collectionsFilter(filtredKeys, false)
const collectionKeys = collectionsFilter(filtredKeys, true)
@ -151,9 +145,7 @@
}
let mainEditor: AnyComponent
$: if (object) getEditorOrDefault(selectedClass, object._class)
$: if (object) getEditorOrDefault(object._class, object._class)
async function getEditorOrDefault (_class: Ref<Class<Doc>> | undefined, defaultClass: Ref<Class<Doc>>): Promise<void> {
let editor = _class !== undefined ? await getEditor(_class) : undefined
if (editor === undefined) {
@ -286,6 +278,8 @@
})
})
})
let minimize: boolean = false
</script>
<ActionContext
@ -300,6 +294,9 @@
{title}
{rightSection}
{object}
bind:minimize
isHeader={false}
isAside={!minimize}
bind:panelWidth
bind:innerWidth
on:update={(ev) => _update(ev.detail)}
@ -307,56 +304,20 @@
dispatch('close')
}}
>
<svelte:fragment slot="navigate-actions">
<svelte:fragment slot="navigator">
<UpDownNavigator element={object} />
</svelte:fragment>
<svelte:fragment slot="subtitle">
<svelte:fragment slot="attributes" let:direction={dir}>
{#if !headerLoading}
{#if headerEditor !== undefined}
<Component is={headerEditor} props={{ object, keys }} />
<Component is={headerEditor} props={{ object, keys, vertical: dir === 'column' }} />
{:else}
<AttributesBar {object} {keys} />
<AttributesBar {object} {keys} vertical={dir === 'column'} />
{/if}
{/if}
</svelte:fragment>
<svelte:fragment slot="properties">
{#if !headerLoading}
<div class="p-4">
{#if headerEditor !== undefined}
<Component is={headerEditor} props={{ object, keys, vertical: true }} />
{:else}
<AttributesBar {object} {keys} vertical />
{/if}
</div>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
{#if displayedIntegrations && displayedIntegrations.length > 0}
<div class="actions-divider" />
{#each displayedIntegrations as pr}
<Tooltip label={pr.label}>
<Button
icon={pr.icon}
size={'medium'}
kind={'transparent'}
highlight={rightSection === pr.presenter}
on:click={() => {
if (rightSection !== pr.presenter) rightSection = pr.presenter
}}
/>
</Tooltip>
{/each}
<Button
icon={IconActivity}
size={'medium'}
kind={'transparent'}
highlight={!rightSection}
on:click={() => {
if (rightSection) rightSection = undefined
}}
/>
{/if}
</svelte:fragment>
<div class="main-editor">
{#if mainEditor}
<Component
@ -376,7 +337,7 @@
</div>
{#each collectionEditors as collection}
{#if collection.editor}
<div class="mt-14">
<div class="mt-6">
<Component
is={collection.editor}
props={{
@ -400,12 +361,4 @@
justify-content: center;
flex-direction: column;
}
.actions-divider {
margin: 0 0.25rem;
min-width: 1px;
width: 1px;
height: 1.5rem;
background-color: var(--divider-color);
}
</style>

View File

@ -298,12 +298,12 @@
}
}
let navFloat: boolean = !(window.innerWidth < 1100)
let navFloat: boolean = window.innerWidth < 1024 ? false : true
const windowResize = (): void => {
if (window.innerWidth < 1100 && !navFloat) {
if (window.innerWidth < 1024 && !navFloat) {
visibileNav = false
navFloat = true
} else if (window.innerWidth >= 1100 && navFloat) {
} else if (window.innerWidth >= 1024 && navFloat) {
navFloat = false
visibileNav = true
}