mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-24 17:30:03 +00:00
parent
6ae2337038
commit
2b8aa36016
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,8 @@ export class TTxViewlet extends TDoc implements TxViewlet {
|
||||
match!: DocumentQuery<Tx>
|
||||
label!: IntlString
|
||||
display!: 'inline' | 'content' | 'emphasized'
|
||||
editable!: boolean
|
||||
hideOnRemove!: boolean
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
|
@ -42,7 +42,7 @@ export class TMessage extends TDoc implements Message {
|
||||
content!: string
|
||||
}
|
||||
|
||||
@Model(chunter.class.Comment, core.class.Doc, DOMAIN_COMMENT)
|
||||
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
|
||||
@UX('Comment' as IntlString)
|
||||
export class TComment extends TAttachedDoc implements Comment {
|
||||
@Prop(TypeString(), 'Message' as IntlString)
|
||||
@ -56,7 +56,7 @@ export class TBacklink extends TComment implements Backlink {
|
||||
backlinkClass!: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
@Model(chunter.class.Attachment, core.class.Doc, DOMAIN_ATTACHMENT)
|
||||
@Model(chunter.class.Attachment, core.class.AttachedDoc, DOMAIN_ATTACHMENT)
|
||||
@UX('File' as IntlString)
|
||||
export class TAttachment extends TAttachedDoc implements Attachment {
|
||||
@Prop(TypeString(), 'Name' as IntlString)
|
||||
@ -138,9 +138,20 @@ export function createModel (builder: Builder): void {
|
||||
txClass: core.class.TxCreateDoc,
|
||||
component: chunter.activity.TxCommentCreate,
|
||||
label: chunter.string.LeftComment,
|
||||
display: 'content'
|
||||
display: 'content',
|
||||
editable: true,
|
||||
hideOnRemove: true
|
||||
}, chunter.ids.TxCommentCreate)
|
||||
|
||||
// We need to define this one, to hide default attached object removed case
|
||||
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
|
||||
objectClass: chunter.class.Comment,
|
||||
icon: chunter.icon.Chunter,
|
||||
txClass: core.class.TxRemoveDoc,
|
||||
display: 'inline',
|
||||
hideOnRemove: true
|
||||
}, chunter.ids.TxCommentRemove)
|
||||
|
||||
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
|
||||
objectClass: chunter.class.Attachment,
|
||||
icon: chunter.icon.Attachment,
|
||||
|
@ -39,6 +39,7 @@ export default mergeIds(chunterId, chunter, {
|
||||
},
|
||||
ids: {
|
||||
TxCommentCreate: '' as Ref<TxViewlet>,
|
||||
TxCommentRemove: '' as Ref<TxViewlet>,
|
||||
TxAttachmentCreate: '' as Ref<TxViewlet>
|
||||
},
|
||||
activity: {
|
||||
|
@ -24,14 +24,29 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let content: string = ''
|
||||
export let showSend = true
|
||||
|
||||
let textEditor: TextEditor
|
||||
|
||||
export function submit (): void {
|
||||
textEditor.submit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ref-container">
|
||||
<div class="textInput">
|
||||
<div class="inputMsg">
|
||||
<TextEditor on:message={ev => dispatch('message', ev.detail)}/>
|
||||
<TextEditor content={content} bind:this={textEditor} on:content={
|
||||
ev => {
|
||||
dispatch('message', ev.detail)
|
||||
content = ''
|
||||
}
|
||||
}/>
|
||||
</div>
|
||||
<button class="sendButton"><div class="icon"><Send /></div></button>
|
||||
{#if showSend}
|
||||
<button class="sendButton" on:click={submit}><div class="icon"><Send/></div></button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="tool"><Attach /></div>
|
||||
|
@ -31,22 +31,28 @@ import { getClient } from '@anticrm/presentation'
|
||||
|
||||
import contact from '@anticrm/contact'
|
||||
|
||||
export let content: string = ''
|
||||
|
||||
let element: HTMLElement
|
||||
let editor: Editor
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
export function submit (): void {
|
||||
content = editor.getHTML()
|
||||
dispatch('content', content)
|
||||
content = ''
|
||||
editor.commands.clearContent(false)
|
||||
}
|
||||
|
||||
const HandleEnter = Extension.create({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Enter': () => {
|
||||
dispatch('message', this.editor.getHTML())
|
||||
this.editor.commands.clearContent(false)
|
||||
'Enter': () => {
|
||||
submit()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -54,6 +60,7 @@ const HandleEnter = Extension.create({
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
content: content,
|
||||
extensions: [
|
||||
HandleEnter,
|
||||
StarterKit,
|
||||
@ -103,7 +110,6 @@ onDestroy(() => {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div style="width: 100%" bind:this={element}/>
|
||||
|
@ -21,7 +21,7 @@
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let label: IntlString
|
||||
export let direction: TooltipAligment | undefined
|
||||
export let direction: TooltipAligment | undefined = undefined
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let action: (ev?: Event) => Promise<void>
|
||||
|
77
packages/ui/src/components/Menu.svelte
Normal file
77
packages/ui/src/components/Menu.svelte
Normal file
@ -0,0 +1,77 @@
|
||||
<!--
|
||||
// Copyright © 2020 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 type { Asset,IntlString } from '@anticrm/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AnySvelteComponent } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
export let actions: {
|
||||
label: IntlString
|
||||
icon?: Asset | AnySvelteComponent
|
||||
action: () => void | Promise<void>
|
||||
}[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flex-col popup">
|
||||
{#each actions as action}
|
||||
<div class="flex-row-center menu-item" on:click={() => {
|
||||
dispatch('close')
|
||||
action.action()
|
||||
}}>
|
||||
{#if action.icon}
|
||||
<div class="icon">
|
||||
<Icon icon={action.icon} size={'large'} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="label"><Label label={action.label} /></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
padding: .5rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-button-bg-focused);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
box-shadow: 0 .75rem 1.25rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .375rem 1rem .375rem .5rem;
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-right: .75rem;
|
||||
transform-origin: center center;
|
||||
transform: scale(.75);
|
||||
opacity: .3;
|
||||
}
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
&:hover { background-color: var(--theme-button-bg-hovered); }
|
||||
}
|
||||
</style>
|
@ -18,7 +18,7 @@
|
||||
import type { TooltipAligment, AnySvelteComponent, AnyComponent } from '..'
|
||||
import { tooltipstore as tooltip, showTooltip } from '..'
|
||||
|
||||
export let label: IntlString | undefined
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let direction: TooltipAligment | undefined = undefined
|
||||
export let component: AnySvelteComponent | AnyComponent | undefined = undefined
|
||||
export let props: any | undefined = undefined
|
||||
|
8
packages/ui/src/components/icons/Delete.svelte
Normal file
8
packages/ui/src/components/icons/Delete.svelte
Normal file
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
export let size: '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 d="M14.3,2.8h-2.4c-0.3,0-0.6-0.3-0.7-0.6l-0.2-1C10.8,0.5,10.3,0,9.6,0H6.4C5.7,0,5.2,0.5,5,1.2l-0.2,1 C4.7,2.6,4.4,2.8,4.1,2.8H1.7c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h0.5c0.1,1.2,0.4,7.7,0.6,9.9C3,15.1,3.8,15.9,5,16 c1,0,2,0,3,0c0.9,0,1.9,0,2.9,0c1.3,0,2.1-0.9,2.3-2.2c0.2-2.1,0.6-8.7,0.6-9.9h0.5c0.3,0,0.5-0.2,0.5-0.5S14.6,2.8,14.3,2.8z M5.8,2.4l0.2-1c0.1-0.2,0.2-0.4,0.4-0.4h3.2c0.2,0,0.4,0.2,0.4,0.3l0.2,1c0,0.2,0.1,0.3,0.2,0.4H5.7C5.7,2.7,5.8,2.5,5.8,2.4z M12.1,13.6c-0.1,0.8-0.5,1.3-1.2,1.3c-2,0-4,0-5.8,0c-0.9,0-1.1-0.7-1.2-1.3c-0.2-2.1-0.6-8.5-0.6-9.8h9.5 C12.7,5.2,12.3,11.6,12.1,13.6z"/>
|
||||
</svg>
|
@ -74,8 +74,10 @@ export { default as IconExpand } from './components/icons/Expand.svelte'
|
||||
export { default as IconActivity } from './components/icons/Activity.svelte'
|
||||
export { default as IconUp } from './components/icons/Up.svelte'
|
||||
export { default as IconDown } from './components/icons/Down.svelte'
|
||||
export { default as IconDelete } from './components/icons/Delete.svelte'
|
||||
export { default as IconEdit } from './components/icons/Edit.svelte'
|
||||
export { default as IconInfo } from './components/icons/Info.svelte'
|
||||
export { default as Menu } from './components/Menu.svelte'
|
||||
|
||||
export * from './utils'
|
||||
|
||||
|
8
plugins/activity-assets/lang/en.json
Normal file
8
plugins/activity-assets/lang/en.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"string": {
|
||||
"Delete": "Delete",
|
||||
"Edit": "Edit",
|
||||
"Options": "Options",
|
||||
"Edited": "edited"
|
||||
}
|
||||
}
|
@ -13,10 +13,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { loadMetadata } from '@anticrm/platform'
|
||||
import activity from '@anticrm/activity'
|
||||
import { addStringsLoader, loadMetadata } from '@anticrm/platform'
|
||||
import activity, {activityId} from '@anticrm/activity'
|
||||
|
||||
const icons = require('../assets/icons.svg') // eslint-disable-line
|
||||
loadMetadata(activity.icon, {
|
||||
Activity: `${icons}#activity` // eslint-disable-line
|
||||
})
|
||||
|
||||
addStringsLoader(activityId, async (lang: string) => await import(`../lang/${lang}.json`))
|
@ -54,6 +54,7 @@ export interface DisplayTx {
|
||||
// Document in case it is required.
|
||||
doc?: Doc
|
||||
|
||||
updated: boolean
|
||||
removed: boolean
|
||||
}
|
||||
|
||||
@ -136,23 +137,37 @@ class ActivityImpl implements Activity {
|
||||
// We need to sort with with natural order, to build a proper doc values.
|
||||
const allTx = Array.from(txes1).concat(txes2).sort(this.sortByLastModified)
|
||||
const txCUD: Array<TxCUD<Doc>> = this.filterTxCUD(allTx, hierarchy)
|
||||
|
||||
const parents = new Map<Ref<Doc>, DisplayTx>()
|
||||
|
||||
const results: DisplayTx[] = []
|
||||
|
||||
for (const tx of txCUD) {
|
||||
const { collectionCUD, updateCUD, result, tx: ntx } = this.createDisplayTx(tx, parents)
|
||||
|
||||
// We do not need collection object updates, in main list of displayed transactions.
|
||||
if (this.isDisplayTxRequired(collectionCUD, updateCUD, ntx, object)) {
|
||||
// Combine previous update transaction for same field and if same operation and time treshold is ok
|
||||
this.checkIntegratePreviousTx(results, result)
|
||||
results.push(result)
|
||||
|
||||
this.updateRemovedState(result, results)
|
||||
}
|
||||
}
|
||||
console.log('DISPLAY TX', results)
|
||||
return Array.from(results)
|
||||
}
|
||||
|
||||
private updateRemovedState (result: DisplayTx, results: DisplayTx[]): void {
|
||||
if (result.removed) {
|
||||
// We need to mark all transactions for same object as removed as well.
|
||||
for (const t of results) {
|
||||
if (t.tx.objectId === result.tx.objectId) {
|
||||
t.removed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sortByLastModified (a: TxCUD<Doc>, b: TxCUD<Doc>): number {
|
||||
return a.modifiedOn - b.modifiedOn
|
||||
}
|
||||
@ -173,7 +188,7 @@ class ActivityImpl implements Activity {
|
||||
let updateCUD = false
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
tx = (tx as TxCollectionCUD<Doc, AttachedDoc>).tx
|
||||
tx = getCollectionTx((tx as TxCollectionCUD<Doc, AttachedDoc>))
|
||||
collectionCUD = true
|
||||
}
|
||||
let firstTx = parents.get(tx.objectId)
|
||||
@ -185,17 +200,24 @@ class ActivityImpl implements Activity {
|
||||
parents.set(tx.objectId, firstTx)
|
||||
|
||||
// If we have updates also apply them all.
|
||||
updateCUD = this.updateFirstTx(result, firstTx)
|
||||
updateCUD = this.checkUpdateState(result, firstTx)
|
||||
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc) && result.doc !== undefined) {
|
||||
firstTx.removed = true
|
||||
}
|
||||
this.checkRemoveState(hierarchy, tx, firstTx, result)
|
||||
return { collectionCUD, updateCUD, result, tx }
|
||||
}
|
||||
|
||||
updateFirstTx (result: DisplayTx, firstTx: DisplayTx): boolean {
|
||||
private checkRemoveState (hierarchy: Hierarchy, tx: TxCUD<Doc>, firstTx: DisplayTx, result: DisplayTx): void {
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) {
|
||||
firstTx.removed = true
|
||||
result.removed = true
|
||||
}
|
||||
}
|
||||
|
||||
checkUpdateState (result: DisplayTx, firstTx: DisplayTx): boolean {
|
||||
if (this.client.getHierarchy().isDerived(result.tx._class, core.class.TxUpdateDoc) && result.doc !== undefined) {
|
||||
firstTx.doc = TxProcessor.updateDoc2Doc(result.doc, result.tx as TxUpdateDoc<Doc>)
|
||||
firstTx.updated = true
|
||||
result.updated = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -209,6 +231,7 @@ class ActivityImpl implements Activity {
|
||||
txes: [],
|
||||
createTx,
|
||||
updateTx: hierarchy.isDerived(tx._class, core.class.TxUpdateDoc) ? (tx as TxUpdateDoc<Doc>) : undefined,
|
||||
updated: false,
|
||||
removed: false,
|
||||
doc: createTx !== undefined ? TxProcessor.createDoc2Doc(createTx) : undefined
|
||||
}
|
||||
@ -244,6 +267,18 @@ class ActivityImpl implements Activity {
|
||||
}
|
||||
}
|
||||
|
||||
function getCollectionTx (cltx: TxCollectionCUD<Doc, AttachedDoc>): TxCUD<Doc> {
|
||||
if (cltx.tx._class === core.class.TxCreateDoc) {
|
||||
// We need to update tx to contain attachedDoc, attachedClass & collection
|
||||
const create = cltx.tx as TxCreateDoc<AttachedDoc>
|
||||
create.attributes.attachedTo = cltx.objectId
|
||||
create.attributes.attachedToClass = cltx.objectClass
|
||||
create.attributes.collection = cltx.collection
|
||||
return create
|
||||
}
|
||||
return cltx.tx
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an new activity, to listend for displayed transactions in UI.
|
||||
* @param client
|
||||
|
@ -18,18 +18,18 @@
|
||||
import activity from '@anticrm/activity'
|
||||
import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import core, { Class, Doc, Ref, TxCUD, TxUpdateDoc } from '@anticrm/core'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { getResource, IntlString } from '@anticrm/platform'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { AnyComponent, AnySvelteComponent, Component, Icon, Label, TimeSince } from '@anticrm/ui'
|
||||
import type { AttributeModel } from '@anticrm/view'
|
||||
import { buildModel, getObjectPresenter } from '@anticrm/view-resources'
|
||||
import { AnyComponent, AnySvelteComponent, Component, Icon, IconEdit, IconMoreH, Label, Menu, showPopup, TimeSince } from '@anticrm/ui'
|
||||
import type { Action, AttributeModel } from '@anticrm/view'
|
||||
import { buildModel, getActions, getObjectPresenter } from '@anticrm/view-resources'
|
||||
import { activityKey, ActivityKey, DisplayTx } from '../activity'
|
||||
|
||||
export let tx: DisplayTx
|
||||
export let viewlets: Map<ActivityKey, TxViewlet>
|
||||
|
||||
type TxDisplayViewlet =
|
||||
| (Pick<TxViewlet, 'icon' | 'label' | 'display'> & { component?: AnyComponent | AnySvelteComponent })
|
||||
| (Pick<TxViewlet, 'icon' | 'label' | 'display'|'editable' | 'hideOnRemove'> & { component?: AnyComponent | AnySvelteComponent })
|
||||
| undefined
|
||||
|
||||
let ptx: DisplayTx | undefined
|
||||
@ -38,6 +38,9 @@
|
||||
let props: any
|
||||
let employee: EmployeeAccount | undefined
|
||||
let model: AttributeModel[] = []
|
||||
let actions: Action[] = []
|
||||
|
||||
let edit = false
|
||||
|
||||
$: if (tx.tx._id !== ptx?.tx._id) {
|
||||
viewlet = undefined
|
||||
@ -71,7 +74,7 @@
|
||||
const key = activityKey(dtx.tx.objectClass, dtx.tx._class)
|
||||
let viewlet: TxDisplayViewlet = viewlets.get(key)
|
||||
|
||||
props = { tx: dtx.tx, value: dtx.doc }
|
||||
props = { tx: dtx.tx, value: dtx.doc, edit }
|
||||
|
||||
if (viewlet === undefined && dtx.tx._class === core.class.TxCreateDoc) {
|
||||
// Check if we have a class presenter we could have a pseudo viewlet based on class presenter.
|
||||
@ -107,12 +110,40 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: getActions(client, tx.tx.objectClass).then((result) => {
|
||||
actions = result
|
||||
})
|
||||
|
||||
function getValue (utx: TxUpdateDoc<Doc>, key: string): any {
|
||||
return (utx.operations as any)[key]
|
||||
}
|
||||
const showMenu = async (ev: MouseEvent): Promise<void> => {
|
||||
showPopup(Menu, {
|
||||
actions: [{
|
||||
label: activity.string.Edit,
|
||||
icon: IconEdit,
|
||||
action: () => {
|
||||
edit = true
|
||||
props = { ...props, edit }
|
||||
}
|
||||
}, ...actions.map(a => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
action: async () => {
|
||||
const impl = await getResource(a.action)
|
||||
await impl(tx.doc as Doc)
|
||||
}
|
||||
}))
|
||||
]
|
||||
}, ev.target as HTMLElement)
|
||||
}
|
||||
const onCancelEdit = () => {
|
||||
edit = false
|
||||
props = { ...props, edit }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if viewlet !== undefined || model.length > 0}
|
||||
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && tx.removed)) || model.length > 0}
|
||||
<div class="flex-col msgactivity-container">
|
||||
<div class="flex-between">
|
||||
<div class="flex-center icon">
|
||||
@ -132,8 +163,24 @@
|
||||
No employee
|
||||
{/if}
|
||||
</div>
|
||||
{#if viewlet && viewlet.label}
|
||||
<div><Label label={viewlet.label} /></div>
|
||||
{#if viewlet && viewlet?.editable}
|
||||
<div class='edited'>
|
||||
{#if viewlet.label}
|
||||
<Label label={viewlet.label} />
|
||||
{/if}
|
||||
{#if tx.updated}
|
||||
<Label label={activity.string.Edited}/>
|
||||
{/if}
|
||||
<div class="menuOptions" on:click={(ev) => showMenu(ev)}>
|
||||
<IconMoreH size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if viewlet && viewlet.label}
|
||||
<div>
|
||||
<Label label={viewlet.label} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
|
||||
{#each model as m}
|
||||
@ -143,9 +190,9 @@
|
||||
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
|
||||
<div>
|
||||
{#if typeof viewlet.component === 'string'}
|
||||
<Component is={viewlet.component} {props} />
|
||||
<Component is={viewlet.component} {props} on:close={onCancelEdit} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet.component} {...props} />
|
||||
<svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@ -155,9 +202,9 @@
|
||||
{#if viewlet && viewlet.component && viewlet.display !== 'inline'}
|
||||
<div class="content" class:emphasize={viewlet.display === 'emphasized'}>
|
||||
{#if typeof viewlet.component === 'string'}
|
||||
<Component is={viewlet.component} {props} />
|
||||
<Component is={viewlet.component} {props} on:close={onCancelEdit} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet.component} {...props} />
|
||||
<svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@ -189,6 +236,12 @@
|
||||
}
|
||||
// :global(.msgactivity-container > *:last-child::after) { content: none; }
|
||||
|
||||
.menuOptions {
|
||||
margin-left: .5rem;
|
||||
opacity: .6;
|
||||
cursor: pointer;
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
@ -214,6 +267,12 @@
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
|
||||
.edited {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -35,6 +35,12 @@ export interface TxViewlet extends Doc {
|
||||
label?: IntlString
|
||||
// Do component need to be emphasized or not.
|
||||
display: 'inline' | 'content' | 'emphasized'
|
||||
|
||||
// If defined and true, will show context menu with Edit action, and will pass 'edit:true' to viewlet properties.
|
||||
editable?: boolean
|
||||
|
||||
// If defined and true, will hide all transactions from object in case it is deleted.
|
||||
hideOnRemove?: boolean
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
@ -42,6 +48,12 @@ export interface TxViewlet extends Doc {
|
||||
export const activityId = 'activity' as Plugin
|
||||
|
||||
export default plugin(activityId, {
|
||||
string: {
|
||||
Delete: '' as IntlString,
|
||||
Edit: '' as IntlString,
|
||||
Options: '' as IntlString,
|
||||
Edited: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
Activity: '' as Asset
|
||||
},
|
||||
|
@ -2,6 +2,8 @@
|
||||
"string": {
|
||||
"ApplicationLabelChunter": "Chat",
|
||||
"LeftComment": "left a comment",
|
||||
"AddAttachment": "uploaded an attachment"
|
||||
"AddAttachment": "uploaded an attachment",
|
||||
"EditUpdate": "Save...",
|
||||
"EditCancel": "Cancel"
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import type { Doc, Ref, Space } from '@anticrm/core'
|
||||
import type { Comment } from '@anticrm/chunter'
|
||||
import { ReferenceInput } from '@anticrm/text-editor'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { ScrollBox, Grid } from '@anticrm/ui'
|
||||
|
||||
import chunter from '@anticrm/chunter'
|
||||
import CommentPresenter from './CommentPresenter.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let space: Ref<Space>
|
||||
|
||||
let comments: Comment[]
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
$: query.query(chunter.class.Comment, { attachedTo: object._id }, result => { comments = result })
|
||||
|
||||
function onMessage(event: CustomEvent) {
|
||||
client.createDoc(chunter.class.Comment, space, {
|
||||
attachedTo: object._id,
|
||||
message: event.detail
|
||||
})
|
||||
console.log(event.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="msg-board">
|
||||
<ScrollBox vertical stretch noShift>
|
||||
{#if comments}
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
{#each comments as comment}
|
||||
<CommentPresenter value={comment} />
|
||||
{/each}
|
||||
</Grid>
|
||||
{/if}
|
||||
</ScrollBox>
|
||||
</div>
|
||||
<ReferenceInput on:message={onMessage}/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: max-content;
|
||||
|
||||
.msg-board {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 1.5em;
|
||||
min-height: 2rem;
|
||||
height: max-content;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -14,20 +14,55 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { Comment } from "@anticrm/chunter"
|
||||
import type { TxCreateDoc } from "@anticrm/core"
|
||||
import { MessageViewer } from '@anticrm/presentation'
|
||||
import type { Comment } from "@anticrm/chunter";
|
||||
import type { TxCreateDoc } from "@anticrm/core";
|
||||
import { getClient, MessageViewer } from '@anticrm/presentation'
|
||||
import { ReferenceInput } from "@anticrm/text-editor"
|
||||
import { Button } from "@anticrm/ui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import chunter from '../../plugin'
|
||||
|
||||
export let tx: TxCreateDoc<Comment>
|
||||
</script>
|
||||
export let value: Comment
|
||||
export let edit: boolean = false
|
||||
|
||||
<div class="text">
|
||||
<MessageViewer message={tx.attributes.message}/>
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let editing = false
|
||||
|
||||
function onMessage(event: CustomEvent) {
|
||||
client.updateCollection(tx.objectClass, tx.objectSpace, tx.objectId, value.attachedTo, value.attachedToClass, value.collection, {
|
||||
message: event.detail
|
||||
})
|
||||
dispatch('close', false)
|
||||
}
|
||||
let refInput: ReferenceInput
|
||||
</script>
|
||||
<div class='container' class:editing={editing}>
|
||||
<div class="text">
|
||||
{#if edit}
|
||||
<ReferenceInput bind:this={refInput} content={value.message} on:message={onMessage} showSend={false}/>
|
||||
<div class='flex-row-reverse flex-grab'>
|
||||
<Button label={chunter.string.EditCancel} on:click={() => {
|
||||
dispatch('close', false)
|
||||
}}/>
|
||||
<Button label={chunter.string.EditUpdate} on:click={() => refInput.submit()} />
|
||||
</div>
|
||||
{:else}
|
||||
<MessageViewer message={value.message}/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.text {
|
||||
line-height: 150%;
|
||||
color: var(--theme-content-color);
|
||||
.container {
|
||||
.text {
|
||||
line-height: 150%;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
.editing {
|
||||
border: 1px solid var(--primary-button-focused-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -15,7 +15,6 @@
|
||||
|
||||
import CreateChannel from './components/CreateChannel.svelte'
|
||||
import ChannelView from './components/ChannelView.svelte'
|
||||
import Activity from './components/Activity.svelte'
|
||||
import AttachmentsPresenter from './components/AttachmentsPresenter.svelte'
|
||||
import AttachmentPresenter from './components/AttachmentPresenter.svelte'
|
||||
import CommentPresenter from './components/CommentPresenter.svelte'
|
||||
@ -29,7 +28,6 @@ export default async () => ({
|
||||
component: {
|
||||
CreateChannel,
|
||||
ChannelView,
|
||||
Activity,
|
||||
AttachmentsPresenter,
|
||||
AttachmentPresenter,
|
||||
CommentPresenter,
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { IntlString, plugin } from '@anticrm/platform'
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
import type { Space, Doc, Ref, Class, AttachedDoc } from '@anticrm/core'
|
||||
|
||||
@ -75,5 +75,9 @@ export default plugin(chunterId, {
|
||||
},
|
||||
space: {
|
||||
Backlinks: '' as Ref<Space>
|
||||
},
|
||||
string: {
|
||||
EditUpdate: '' as IntlString,
|
||||
EditCancel: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -14,24 +14,23 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { IntlString, Asset, Resource } from '@anticrm/platform'
|
||||
import type { Doc } from '@anticrm/core'
|
||||
import type { Asset, IntlString, Resource } from '@anticrm/platform'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { getClient, createQuery } from '@anticrm/presentation'
|
||||
import type { Ref, Class, Doc } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Menu } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Icon, Label, IconMoreH, IconFile } from '@anticrm/ui'
|
||||
import type { Action, ActionTarget } from '@anticrm/view'
|
||||
import { getActions } from '../utils'
|
||||
import view from '@anticrm/view'
|
||||
|
||||
export let object: Doc
|
||||
|
||||
let actions: Action[] = []
|
||||
let actions: {
|
||||
label: IntlString
|
||||
icon?: Asset
|
||||
action: () => void
|
||||
}[] = []
|
||||
|
||||
const client = getClient()
|
||||
|
||||
getActions(client, object._class).then(result => { actions = result })
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function invokeAction(action: Resource<(object: Doc) => Promise<void>>) {
|
||||
@ -40,46 +39,15 @@
|
||||
await impl(object)
|
||||
}
|
||||
|
||||
getActions(client, object._class).then(result => {
|
||||
actions = result.map(a => ({
|
||||
label: a.label,
|
||||
icon: a.icon,
|
||||
action: () => { invokeAction(a.action) }
|
||||
}) )
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex-col popup">
|
||||
{#each actions as action}
|
||||
<div class="flex-row-center menu-item" on:click={() => { invokeAction(action.action) }}>
|
||||
<div class="icon">
|
||||
<Icon icon={action.icon} size={'large'} />
|
||||
</div>
|
||||
<div class="label"><Label label={action.label} /></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Menu actions={actions} on:close/>
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
padding: .5rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-button-bg-focused);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
box-shadow: 0 .75rem 1.25rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .375rem 1rem .375rem .5rem;
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-right: .75rem;
|
||||
transform-origin: center center;
|
||||
transform: scale(.75);
|
||||
opacity: .3;
|
||||
}
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
&:hover { background-color: var(--theme-button-bg-hovered); }
|
||||
}
|
||||
</style>
|
||||
|
@ -31,7 +31,7 @@ import { showPopup } from '@anticrm/ui'
|
||||
import {buildModel} from './utils'
|
||||
|
||||
export { Table }
|
||||
export { buildModel, getObjectPresenter } from './utils'
|
||||
export { buildModel, getObjectPresenter, getActions } from './utils'
|
||||
|
||||
function Delete(object: Doc): void {
|
||||
showPopup(MessageBox, {
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { Ref, Class, Obj, FindOptions, Doc, Client } from '@anticrm/core'
|
||||
import type { Ref, Class, Obj, FindOptions, Doc, Client, FindResult } from '@anticrm/core'
|
||||
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
|
||||
import type { Action, ActionTarget, BuildModelOptions } from '@anticrm/view'
|
||||
|
||||
@ -117,7 +117,7 @@ function filterActions(client: Client, _class: Ref<Class<Obj>>, targets: ActionT
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getActions(client: Client, _class: Ref<Class<Obj>>) {
|
||||
export async function getActions(client: Client, _class: Ref<Class<Obj>>): Promise<FindResult<Action>> {
|
||||
const targets = await client.findAll(view.class.ActionTarget, {})
|
||||
return await client.findAll(view.class.Action, { _id: { $in: filterActions(client, _class, targets) }})
|
||||
}
|
||||
|
@ -17,10 +17,9 @@
|
||||
// import type { IntlString, Asset, Resource } from '@anticrm/platform'
|
||||
import type { Ref, State, Class, Obj } from '@anticrm/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Label, showPopup } from '@anticrm/ui'
|
||||
import { Label, showPopup, IconDelete as Delete } from '@anticrm/ui'
|
||||
import { getClient, MessageBox } from '@anticrm/presentation'
|
||||
import type { Kanban } from '@anticrm/view'
|
||||
import Delete from './icons/Delete.svelte'
|
||||
|
||||
import workbench from '../plugin'
|
||||
import view from '@anticrm/view'
|
||||
|
@ -116,6 +116,10 @@ class TServerStorage implements ServerStorage {
|
||||
attachedTo = (await this.findAll(_class, { _id }))[0]
|
||||
const txFactory = new TxFactory(tx.modifiedBy)
|
||||
return [txFactory.createTxUpdateDoc(_class, attachedTo.space, _id, { $inc: { [colTx.collection]: 1 } })]
|
||||
} else if (colTx.tx._class === core.class.TxRemoveDoc) {
|
||||
attachedTo = (await this.findAll(_class, { _id }))[0]
|
||||
const txFactory = new TxFactory(tx.modifiedBy)
|
||||
return [txFactory.createTxUpdateDoc(_class, attachedTo.space, _id, { $inc: { [colTx.collection]: -1 } })]
|
||||
}
|
||||
}
|
||||
return []
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user