fix: make todos and commands working in meeting minutes (#7244)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-12-02 21:15:49 +07:00 committed by GitHub
parent db5890b831
commit 1d1e9cdd98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 149 additions and 135 deletions

View File

@ -19,6 +19,7 @@
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import Label from './Label.svelte' import Label from './Label.svelte'
export let id: string | undefined = undefined
export let label: IntlString export let label: IntlString
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
@ -27,7 +28,7 @@
export let invisible: boolean = false export let invisible: boolean = false
</script> </script>
<div class="antiSection"> <div class="antiSection" {id}>
{#if showHeader} {#if showHeader}
<div class="antiSection-header" class:high class:invisible> <div class="antiSection-header" class:high class:invisible>
{#if icon} {#if icon}

View File

@ -22,7 +22,7 @@
} from '@hcengineering/activity' } from '@hcengineering/activity'
import { Doc, Ref, SortingOrder } from '@hcengineering/core' import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Grid, Label, Spinner, location, Lazy } from '@hcengineering/ui' import { Grid, Label, Section, Spinner, location, Lazy } from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import ActivityExtensionComponent from './ActivityExtension.svelte' import ActivityExtensionComponent from './ActivityExtension.svelte'
@ -235,77 +235,81 @@
$: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending) $: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
</script> </script>
<div class="antiSection-header high mt-9" class:invisible={transparent}> <div class="step-tb-6">
<span class="antiSection-header__title flex-row-center"> <Section label={activity.string.Activity} icon={activity.icon.Activity}>
<Label label={activity.string.Activity} /> <svelte:fragment slot="header">
{#if isLoading} {#if isLoading}
<div class="ml-1"> <div class="ml-1">
<Spinner size="small" /> <Spinner size="small" />
</div> </div>
{/if} {/if}
</span> <ActivityFilter
<ActivityFilter messages={allMessages}
messages={allMessages} {object}
{object} on:update={(e) => {
on:update={(e) => { filteredMessages = e.detail
filteredMessages = e.detail }}
}} bind:isNewestFirst
bind:isNewestFirst />
/> </svelte:fragment>
</div>
{#if isNewestFirst && showCommenInput} <svelte:fragment slot="content">
<div class="ref-input newest-first"> {#if isNewestFirst && showCommenInput}
<ActivityExtensionComponent <div class="ref-input newest-first">
kind="input" <ActivityExtensionComponent
{extensions} kind="input"
props={{ object, boundary, focusIndex, withTypingInfo: true }} {extensions}
/> props={{ object, boundary, focusIndex, withTypingInfo: true }}
</div>
{/if}
<div
class="p-activity select-text"
id={activity.string.Activity}
class:newest-first={isNewestFirst}
bind:this={activityBox}
>
{#if filteredMessages.length}
<Grid column={1} rowGap={0}>
{#each filteredMessages as message, index}
{@const canGroup = canGroupMessages(message, filteredMessages[index - 1])}
{#if selectedMessageId}
<ActivityMessagePresenter
value={message}
doc={object}
hideLink={true}
type={canGroup ? 'short' : 'default'}
isHighlighted={selectedMessageId === message._id}
withShowMore
/> />
{:else} </div>
<Lazy> {/if}
<ActivityMessagePresenter <div
value={message} class="p-activity select-text"
doc={object} id={activity.string.Activity}
hideLink={true} class:newest-first={isNewestFirst}
type={canGroup ? 'short' : 'default'} bind:this={activityBox}
isHighlighted={selectedMessageId === message._id} >
withShowMore {#if filteredMessages.length}
/> <Grid column={1} rowGap={0}>
</Lazy> {#each filteredMessages as message, index}
{@const canGroup = canGroupMessages(message, filteredMessages[index - 1])}
{#if selectedMessageId}
<ActivityMessagePresenter
value={message}
doc={object}
hideLink={true}
type={canGroup ? 'short' : 'default'}
isHighlighted={selectedMessageId === message._id}
withShowMore
/>
{:else}
<Lazy>
<ActivityMessagePresenter
value={message}
doc={object}
hideLink={true}
type={canGroup ? 'short' : 'default'}
isHighlighted={selectedMessageId === message._id}
withShowMore
/>
</Lazy>
{/if}
{/each}
</Grid>
{/if} {/if}
{/each} </div>
</Grid> {#if showCommenInput && !isNewestFirst}
{/if} <div class="ref-input oldest-first">
<ActivityExtensionComponent
kind="input"
{extensions}
props={{ object, boundary, focusIndex, withTypingInfo: true }}
/>
</div>
{/if}
</svelte:fragment>
</Section>
</div> </div>
{#if showCommenInput && !isNewestFirst}
<div class="ref-input oldest-first">
<ActivityExtensionComponent
kind="input"
{extensions}
props={{ object, boundary, focusIndex, withTypingInfo: true }}
/>
</div>
{/if}
<style lang="scss"> <style lang="scss">
.ref-input { .ref-input {

View File

@ -14,16 +14,16 @@
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
import { Doc, Ref, SortingOrder } from '@hcengineering/core' import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { ActionIcon, eventToHTMLElement, Icon, Label, showPopup } from '@hcengineering/ui' import { Button, eventToHTMLElement, Icon, Label, showPopup } from '@hcengineering/ui'
import { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity' import view from '@hcengineering/view'
import activity from '../plugin' import activity from '../plugin'
import FilterPopup from './FilterPopup.svelte' import FilterPopup from './FilterPopup.svelte'
import IconClose from './icons/Close.svelte' import IconClose from './icons/Close.svelte'
import IconFilter from './icons/Filter.svelte'
import { sortActivityMessages } from '../activityMessagesUtils' import { sortActivityMessages } from '../activityMessagesUtils'
export let messages: ActivityMessage[] export let messages: ActivityMessage[]
@ -146,4 +146,6 @@
{/if} {/if}
{/if} {/if}
<div class="w-4 min-w-4 max-w-4" /> <div class="w-4 min-w-4 max-w-4" />
<ActionIcon icon={IconFilter} size={'medium'} action={handleOptions} /> <div class="buttons-group small-gap pr-2">
<Button icon={view.icon.Configure} size={'small'} kind={'ghost'} on:click={handleOptions} />
</div>

View File

@ -68,7 +68,7 @@
let preference: ViewletPreference | undefined let preference: ViewletPreference | undefined
</script> </script>
<Section label={contact.string.Members} icon={IconMembersOutline}> <Section id="members" label={contact.string.Members} icon={IconMembersOutline}>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<div class="buttons-group xsmall-gap"> <div class="buttons-group xsmall-gap">
<ViewletSelector <ViewletSelector

View File

@ -33,15 +33,13 @@
<Section label={love.string.MeetingMinutes} icon={love.icon.Cam}> <Section label={love.string.MeetingMinutes} icon={love.icon.Cam}>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<div class="flex-row-center gap-2 reverse"> <ViewletsSettingButton
<ViewletsSettingButton viewletQuery={{ _id: love.viewlet.TableMeetingMinutesEmbedded }}
viewletQuery={{ _id: love.viewlet.TableMeetingMinutesEmbedded }} kind={'tertiary'}
kind={'tertiary'} bind:viewlet
bind:viewlet bind:loading
bind:loading bind:preference
bind:preference />
/>
</div>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">

View File

@ -250,37 +250,39 @@
}) })
) )
} }
if (withSideMenu) { }
optionalExtensions.push(
LeftMenuExtension.configure({ if (withSideMenu) {
width: 20, optionalExtensions.push(
height: 20, LeftMenuExtension.configure({
marginX: 8, width: 20,
className: 'tiptap-left-menu', height: 20,
icon: view.icon.Add, marginX: 8,
iconProps: { className: 'tiptap-left-menu',
className: 'svg-tiny', icon: view.icon.Add,
fill: 'currentColor' iconProps: {
}, className: 'svg-tiny',
items: [ fill: 'currentColor'
...(canEmbedImages ? [{ id: 'image', label: textEditor.string.Image, icon: view.icon.Image }] : []), },
{ id: 'table', label: textEditor.string.Table, icon: view.icon.Table2 }, items: [
{ id: 'code-block', label: textEditor.string.CodeBlock, icon: view.icon.CodeBlock }, ...(canEmbedImages ? [{ id: 'image', label: textEditor.string.Image, icon: view.icon.Image }] : []),
{ id: 'separator-line', label: textEditor.string.SeparatorLine, icon: view.icon.SeparatorLine }, { id: 'table', label: textEditor.string.Table, icon: view.icon.Table2 },
{ id: 'todo-list', label: textEditor.string.TodoList, icon: view.icon.TodoList }, { id: 'code-block', label: textEditor.string.CodeBlock, icon: view.icon.CodeBlock },
{ id: 'drawing-board', label: textEditor.string.DrawingBoard, icon: IconScribble as any } { id: 'separator-line', label: textEditor.string.SeparatorLine, icon: view.icon.SeparatorLine },
], { id: 'todo-list', label: textEditor.string.TodoList, icon: view.icon.TodoList },
handleSelect: handleLeftMenuClick { id: 'drawing-board', label: textEditor.string.DrawingBoard, icon: IconScribble as any }
}) ],
handleSelect: handleLeftMenuClick
})
)
}
if (withInlineCommands) {
optionalExtensions.push(
InlineCommandsExtension.configure(
inlineCommandsConfig(handleLeftMenuClick, attachFile === undefined || !canEmbedImages ? ['image'] : [])
) )
} )
if (withInlineCommands) {
optionalExtensions.push(
InlineCommandsExtension.configure(
inlineCommandsConfig(handleLeftMenuClick, attachFile === undefined || !canEmbedImages ? ['image'] : [])
)
)
}
} }
let inputImage: HTMLInputElement let inputImage: HTMLInputElement

View File

@ -41,10 +41,11 @@
editor.off('selectionUpdate', handleSelectionUpdate) editor.off('selectionUpdate', handleSelectionUpdate)
}) })
$: todoable = objectId !== undefined && objectClass !== undefined
$: todoId = node.attrs.todoid as Ref<ToDo> $: todoId = node.attrs.todoid as Ref<ToDo>
$: userId = node.attrs.userid as Ref<Person> $: userId = node.attrs.userid as Ref<Person>
$: checked = node.attrs.checked ?? false $: checked = node.attrs.checked ?? false
$: readonly = !editor.isEditable || objectId === undefined $: readonly = !editor.isEditable || (!todoable && todoId != null)
let todo: ToDo | undefined = undefined let todo: ToDo | undefined = undefined
$: query.query( $: query.query(
@ -59,7 +60,7 @@
) )
async function syncTodo (todo: ToDo | undefined): Promise<void> { async function syncTodo (todo: ToDo | undefined): Promise<void> {
if (todo !== undefined) { if (todo !== undefined && todo.attachedTo === objectId && todo.attachedToClass === objectClass) {
const todoChecked = todo.doneOn != null const todoChecked = todo.doneOn != null
if (todo._id !== todoId || todo.user !== userId || todoChecked !== checked) { if (todo._id !== todoId || todo.user !== userId || todoChecked !== checked) {
updateAttributes({ updateAttributes({
@ -212,16 +213,18 @@
class:hovered class:hovered
class:focused class:focused
> >
<div class="flex-center assignee" contenteditable="false"> {#if todoable}
<EmployeePresenter <div class="flex-center assignee" contenteditable="false">
value={userId} <EmployeePresenter
disabled={readonly} value={userId}
avatarSize={'card'} disabled={readonly}
shouldShowName={false} avatarSize={'card'}
shouldShowPlaceholder shouldShowName={false}
onEmployeeEdit={handleAssigneeEdit} shouldShowPlaceholder
/> onEmployeeEdit={handleAssigneeEdit}
</div> />
</div>
{/if}
<div class="flex-center todo-check" contenteditable="false"> <div class="flex-center todo-check" contenteditable="false">
<CheckBox {readonly} {checked} on:value={markDone} kind={'positive'} size={'medium'} /> <CheckBox {readonly} {checked} on:value={markDone} kind={'positive'} size={'medium'} />

View File

@ -56,23 +56,24 @@ function isTodoableClass (objectClass: Ref<Class<Doc>>): boolean {
} }
} }
function isTodoable (mode: TextEditorMode, objectClass?: Ref<Class<Doc>>): boolean { function isTodoable (mode: TextEditorMode): boolean {
return mode === 'full' && objectClass !== undefined && isTodoableClass(objectClass) return mode === 'full'
} }
export function createTodoItemExtension (mode: TextEditorMode, ctx: any): AnyExtension | undefined { export function createTodoItemExtension (mode: TextEditorMode, ctx: any): AnyExtension | undefined {
if (!isTodoable(mode, ctx.objectClass)) { if (!isTodoable(mode)) {
return return
} }
const { objectId, objectClass, objectSpace } = ctx const { objectId, objectClass, objectSpace } = ctx
const componentProps = isTodoableClass(objectClass) ? { objectId, objectClass, objectSpace } : {}
return TodoItemExtension.extend({ return TodoItemExtension.extend({
addNodeView () { addNodeView () {
return SvelteNodeViewRenderer(ToDoItemNodeView, { return SvelteNodeViewRenderer(ToDoItemNodeView, {
contentAs: 'li', contentAs: 'li',
contentClass: 'todo-item', contentClass: 'todo-item',
componentProps: { objectId, objectClass, objectSpace } componentProps
}) })
} }
}).configure({ }).configure({
@ -83,7 +84,7 @@ export function createTodoItemExtension (mode: TextEditorMode, ctx: any): AnyExt
} }
export function createTodoListExtension (mode: TextEditorMode, ctx: any): AnyExtension | undefined { export function createTodoListExtension (mode: TextEditorMode, ctx: any): AnyExtension | undefined {
if (!isTodoable(mode, ctx.objectClass)) { if (!isTodoable(mode)) {
return return
} }

View File

@ -95,9 +95,9 @@
{/if} {/if}
<ModernButton <ModernButton
icon={view.icon.Configure} icon={view.icon.Configure}
label={view.string.Show}
{kind} {kind}
size={'small'} size={'small'}
iconSize={'small'}
{disabled} {disabled}
{pressed} {pressed}
tooltip={{ label: view.string.CustomizeView, direction: 'bottom' }} tooltip={{ label: view.string.CustomizeView, direction: 'bottom' }}

View File

@ -14,7 +14,7 @@ export class CommonRecruitingPage extends CalendarPage {
readonly inputComment = (): Locator => this.page.locator('div.text-input div.tiptap') readonly inputComment = (): Locator => this.page.locator('div.text-input div.tiptap')
readonly buttonSendComment = (): Locator => this.page.locator('g#Send') readonly buttonSendComment = (): Locator => this.page.locator('g#Send')
readonly textComment = (): Locator => this.page.locator('div.showMore-content p') readonly textComment = (): Locator => this.page.locator('div.showMore-content p')
readonly inputAddAttachment = (): Locator => this.page.locator('div.antiSection #file') readonly inputAddAttachment = (): Locator => this.page.locator('div.antiSection #file').first()
readonly textAttachmentName = (): Locator => this.page.locator('div.attachment-container > a') readonly textAttachmentName = (): Locator => this.page.locator('div.attachment-container > a')
readonly buttonCreateFirstReview = (): Locator => this.page.locator('span:has-text("Create review")') readonly buttonCreateFirstReview = (): Locator => this.page.locator('span:has-text("Create review")')
readonly buttonMoreActions = (): Locator => readonly buttonMoreActions = (): Locator =>

View File

@ -15,9 +15,12 @@ export class CompanyDetailsPage extends CommonRecruitingPage {
readonly buttonLocation = (): Locator => readonly buttonLocation = (): Locator =>
this.page.locator('//span[text()="Location"]/following-sibling::div[1]/button/span') this.page.locator('//span[text()="Location"]/following-sibling::div[1]/button/span')
readonly addMemberButton = (): Locator => this.page.locator('[id="contact\\:string\\:AddMember"]') readonly addMemberButton = (): Locator =>
this.page.locator('div.antiSection[id="members"]').locator('[id="contact\\:string\\:AddMember"]')
readonly selectMember = (memberName: string): Locator => this.page.getByRole('button', { name: memberName }) readonly selectMember = (memberName: string): Locator => this.page.getByRole('button', { name: memberName })
readonly member = (memberName: string): Locator => this.page.getByRole('link', { name: memberName }) readonly member = (memberName: string): Locator =>
this.page.locator('div.antiSection[id="members"]').getByRole('link', { name: memberName })
async checkCompany (data: NewCompany): Promise<void> { async checkCompany (data: NewCompany): Promise<void> {
await expect(this.inputName()).toHaveValue(data.name) await expect(this.inputName()).toHaveValue(data.name)