Edit comments (#386)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2021-11-29 18:11:27 +07:00 committed by GitHub
parent 6ae2337038
commit 2b8aa36016
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1501 additions and 1273 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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 {

View File

@ -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,

View File

@ -39,6 +39,7 @@ export default mergeIds(chunterId, chunter, {
},
ids: {
TxCommentCreate: '' as Ref<TxViewlet>,
TxCommentRemove: '' as Ref<TxViewlet>,
TxAttachmentCreate: '' as Ref<TxViewlet>
},
activity: {

View File

@ -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>

View File

@ -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}/>

View File

@ -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>

View 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>

View File

@ -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

View 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>

View File

@ -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'

View File

@ -0,0 +1,8 @@
{
"string": {
"Delete": "Delete",
"Edit": "Edit",
"Options": "Options",
"Edited": "edited"
}
}

View File

@ -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`))

View File

@ -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

View File

@ -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;

View File

@ -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
},

View File

@ -2,6 +2,8 @@
"string": {
"ApplicationLabelChunter": "Chat",
"LeftComment": "left a comment",
"AddAttachment": "uploaded an attachment"
"AddAttachment": "uploaded an attachment",
"EditUpdate": "Save...",
"EditCancel": "Cancel"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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
}
})

View File

@ -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>

View File

@ -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, {

View File

@ -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) }})
}

View File

@ -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'

View File

@ -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