mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 21:45:01 +02:00
New Activity - squashed (#4032)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> Co-authored-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
co-authored by
Andrey Sobolev
parent
119d65653a
commit
ea3eb4af83
+102
@@ -0,0 +1,102 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 notification, { DocAttributeUpdates } from '@hcengineering/notification'
|
||||
|
||||
import { IconAdd, IconDelete, Label } from '@hcengineering/ui'
|
||||
import { personAccountByIdStore, PersonAccountRefPresenter } from '@hcengineering/contact-resources'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
export let value: DocAttributeUpdates
|
||||
|
||||
$: removed = getAccountRefs(value.removed)
|
||||
$: added = getAccountRefs(value.added.length > 0 ? value.added : value.set)
|
||||
|
||||
function getAccountRefs (values: DocAttributeUpdates['removed' | 'added' | 'set']): Ref<PersonAccount>[] {
|
||||
const persons = new Set<Ref<Person>>()
|
||||
|
||||
return values.filter((value) => {
|
||||
const account = $personAccountByIdStore.get(value as Ref<PersonAccount>)
|
||||
|
||||
if (account === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (persons.has(account.person)) {
|
||||
return false
|
||||
}
|
||||
|
||||
persons.add(account.person)
|
||||
return true
|
||||
}) as Ref<PersonAccount>[]
|
||||
}
|
||||
|
||||
$: hasDifferentChanges = added.length > 0 && removed.length > 0
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="label">
|
||||
{#if hasDifferentChanges}
|
||||
<Label label={notification.string.ChangedCollaborators} />:
|
||||
{:else if added.length > 0}
|
||||
<Label label={notification.string.NewCollaborators} />:
|
||||
{:else if removed.length > 0}
|
||||
<Label label={notification.string.RemovedCollaborators} />:
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if added.length > 0}
|
||||
<div class="row">
|
||||
{#if hasDifferentChanges}
|
||||
<IconAdd size={'x-small'} fill={'var(--theme-trans-color)'} />
|
||||
{/if}
|
||||
{#each added as add}
|
||||
<PersonAccountRefPresenter inline value={add} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="antiHSpacer"></div>
|
||||
{#if removed.length > 0}
|
||||
<div class="row">
|
||||
{#if hasDifferentChanges}
|
||||
<IconDelete size={'x-small'} fill={'var(--theme-trans-color)'} />
|
||||
{/if}
|
||||
{#each removed as remove}
|
||||
<PersonAccountRefPresenter inline value={remove} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { ActivityMessageExtension, ActivityMessageExtensionKind } from '@hcengineering/notification'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
|
||||
export let kind: ActivityMessageExtensionKind
|
||||
export let extensions: ActivityMessageExtension[] = []
|
||||
export let props: Record<string, any> = {}
|
||||
</script>
|
||||
|
||||
{#each extensions as extension}
|
||||
{#each extension.components as component}
|
||||
{#if component.kind === kind}
|
||||
<Component is={component.component} {props} on:close on:open />
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { DisplayActivityMessage } from '@hcengineering/notification'
|
||||
import view from '@hcengineering/view'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
|
||||
export let value: DisplayActivityMessage
|
||||
export let showNotify: boolean = false
|
||||
export let isHighlighted: boolean = false
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let hasNotifyActions = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: objectPresenter = hierarchy.classHierarchyMixin(value._class as Ref<Class<Doc>>, view.mixin.ObjectPresenter)
|
||||
</script>
|
||||
|
||||
{#if objectPresenter}
|
||||
<Component
|
||||
is={objectPresenter.presenter}
|
||||
props={{
|
||||
value,
|
||||
showNotify,
|
||||
isHighlighted,
|
||||
isSelected,
|
||||
shouldScroll,
|
||||
embedded,
|
||||
hasActionsMenu,
|
||||
hasNotifyActions,
|
||||
onClick
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Person } from '@hcengineering/contact'
|
||||
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import core from '@hcengineering/core/lib/component'
|
||||
import notification, {
|
||||
DisplayActivityMessage,
|
||||
ActivityMessageExtension,
|
||||
DocUpdateMessageViewlet,
|
||||
ChatMessageViewlet
|
||||
} from '@hcengineering/notification'
|
||||
import { Action, ActionIcon, IconMoreH, Label, showPopup } from '@hcengineering/ui'
|
||||
import { getActions, Menu } from '@hcengineering/view-resources'
|
||||
import { getDisplayTime } from '@hcengineering/core'
|
||||
|
||||
import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte'
|
||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||
|
||||
export let message: DisplayActivityMessage
|
||||
export let parentMessage: DisplayActivityMessage | undefined
|
||||
export let viewlet: DocUpdateMessageViewlet | ChatMessageViewlet | undefined
|
||||
export let person: Person | undefined = undefined
|
||||
export let actions: Action[] = []
|
||||
export let excludedActions: string[] = []
|
||||
export let hasNotifyActions = false
|
||||
export let showNotify: boolean = false
|
||||
export let isHighlighted: boolean = false
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
let allActionIds: string[] = []
|
||||
const notifyActions = [
|
||||
notification.action.MarkAsUnreadInboxNotification,
|
||||
notification.action.MarkAsReadInboxNotification,
|
||||
notification.action.DeleteInboxNotification
|
||||
]
|
||||
|
||||
let element: HTMLDivElement | undefined = undefined
|
||||
let extensions: ActivityMessageExtension[] = []
|
||||
let isActionMenuOpened = false
|
||||
|
||||
$: getActions(client, message, notification.class.ActivityMessage).then((res) => {
|
||||
allActionIds = res.map(({ _id }) => _id)
|
||||
})
|
||||
|
||||
function scrollToMessage () {
|
||||
if (element && shouldScroll) {
|
||||
element.scrollIntoView({ behavior: 'auto', block: 'end' })
|
||||
shouldScroll = false
|
||||
}
|
||||
}
|
||||
|
||||
$: if (element && shouldScroll) {
|
||||
setTimeout(scrollToMessage, 100)
|
||||
}
|
||||
|
||||
client
|
||||
.findAll(notification.class.ActivityMessageExtension, { ofMessage: message._class })
|
||||
.then((res: ActivityMessageExtension[]) => {
|
||||
extensions = res
|
||||
})
|
||||
|
||||
function handleActionMenuOpened () {
|
||||
isActionMenuOpened = true
|
||||
}
|
||||
|
||||
function handleActionMenuClosed () {
|
||||
isActionMenuOpened = false
|
||||
}
|
||||
|
||||
$: key = parentMessage ? `${message._id}_${parentMessage._id}` : message._id
|
||||
$: actualExcludedActions = hasNotifyActions ? excludedActions : [...notifyActions, ...excludedActions]
|
||||
|
||||
function showMenu (ev: MouseEvent) {
|
||||
showPopup(
|
||||
Menu,
|
||||
{
|
||||
object: message,
|
||||
baseMenuClass: notification.class.ActivityMessage,
|
||||
excludedActions: actualExcludedActions,
|
||||
actions
|
||||
},
|
||||
ev.target as HTMLElement,
|
||||
handleActionMenuClosed
|
||||
)
|
||||
handleActionMenuOpened()
|
||||
}
|
||||
|
||||
$: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined
|
||||
$: withActionMenu =
|
||||
!embedded &&
|
||||
hasActionsMenu &&
|
||||
(actions.length > 0 || allActionIds.some((id) => !actualExcludedActions.includes(id)))
|
||||
</script>
|
||||
|
||||
{#if !isHidden}
|
||||
{#key key}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
bind:this={element}
|
||||
id={message._id}
|
||||
class="root clear-mins"
|
||||
class:clickable={!!onClick}
|
||||
class:highlighted={isHighlighted}
|
||||
class:selected={isSelected}
|
||||
class:embedded
|
||||
on:click={onClick}
|
||||
>
|
||||
{#if showNotify && !embedded}
|
||||
<div class="notify" />
|
||||
{/if}
|
||||
{#if !embedded}
|
||||
<div class="min-w-6 mt-1">
|
||||
{#if person}
|
||||
<Avatar size="medium" avatar={person.avatar} name={person.name} />
|
||||
{:else}
|
||||
<SystemAvatar size="medium" />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="embeddedMarker" />
|
||||
{/if}
|
||||
<div class="content ml-2 w-full clear-mins">
|
||||
<div class="header clear-mins">
|
||||
{#if person}
|
||||
<EmployeePresenter value={person} shouldShowAvatar={false} />
|
||||
{:else}
|
||||
<span class="strong">
|
||||
<Label label={core.string.System} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<slot name="header" />
|
||||
|
||||
<span class="text-sm">{getDisplayTime(message.createdOn ?? 0)}</span>
|
||||
</div>
|
||||
|
||||
<slot name="content" />
|
||||
|
||||
<ActivityMessageExtensionComponent kind="footer" {extensions} props={{ object: message }} />
|
||||
|
||||
{#if parentMessage}
|
||||
<div class="mt-2" />
|
||||
<ActivityMessagePresenter value={parentMessage} embedded />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actions clear-mins flex flex-gap-2 items-center"
|
||||
class:menuShowed={isActionMenuOpened || message.isPinned}
|
||||
>
|
||||
<ActivityMessageExtensionComponent
|
||||
kind="action"
|
||||
{extensions}
|
||||
props={{ object: message }}
|
||||
on:close={handleActionMenuClosed}
|
||||
on:open={handleActionMenuOpened}
|
||||
/>
|
||||
{#if withActionMenu}
|
||||
<ActionIcon icon={IconMoreH} size="small" action={showMenu} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes highlight {
|
||||
50% {
|
||||
background-color: var(--theme-warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding: 0.75rem 0.75rem 0.75rem 1.25rem;
|
||||
border-radius: 8px;
|
||||
gap: 1rem;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
animation: highlight 5000ms ease-in-out;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--highlight-select);
|
||||
}
|
||||
|
||||
&.embedded {
|
||||
background: var(--theme-navpanel-icons-divider);
|
||||
padding: 0;
|
||||
|
||||
.content {
|
||||
padding: 0.75rem 0.75rem 0.75rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
|
||||
&.menuShowed {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:hover > .actions {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:hover:not(.embedded) {
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 0.875rem;
|
||||
color: var(--theme-halfcontent-color);
|
||||
margin-bottom: 0.25rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 3.5rem);
|
||||
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notify {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.25rem;
|
||||
height: 0.5rem;
|
||||
width: 0.5rem;
|
||||
background-color: var(--theme-inbox-notify);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.embeddedMarker {
|
||||
width: 6px;
|
||||
background: var(--theme-link-color);
|
||||
}
|
||||
</style>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { ActionIcon } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view'
|
||||
import { ActivityMessage } from '@hcengineering/notification'
|
||||
|
||||
export let object: ActivityMessage
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function toggleMessagePinning (): Promise<void> {
|
||||
await client.update(object, { isPinned: !object.isPinned })
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionIcon
|
||||
icon={view.icon.Pin}
|
||||
iconProps={object.isPinned ? { fill: '#3265cb' } : undefined}
|
||||
size="medium"
|
||||
action={toggleMessagePinning}
|
||||
/>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { DocNavLink } from '@hcengineering/view-resources'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import notification, { ChatMessage, ChatMessageViewlet } from '@hcengineering/notification'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
|
||||
import { getLinkData, LinkData } from '../../activityMessagesUtils'
|
||||
|
||||
export let message: ChatMessage
|
||||
export let person: Person | undefined
|
||||
export let viewlet: ChatMessageViewlet | undefined
|
||||
export let object: Doc | undefined
|
||||
export let parentObject: Doc | undefined
|
||||
|
||||
let linkData: LinkData | undefined = undefined
|
||||
|
||||
$: getLinkData(message, object, parentObject, person).then((data) => {
|
||||
linkData = data
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if viewlet?.label}
|
||||
<span class="text-sm lower"> <Label label={viewlet.label} /></span>
|
||||
|
||||
{#if linkData}
|
||||
<span class="text-sm lower"><Label label={linkData.preposition} /></span>
|
||||
<span class="text-sm">
|
||||
<DocNavLink {object} component={linkData.panelComponent} shrink={0}>
|
||||
<span class="overflow-label select-text">{linkData.title}</span>
|
||||
</DocNavLink>
|
||||
</span>
|
||||
{#if message.isEdited}
|
||||
<span class="text-sm lower"><Label label={notification.string.Edited} /></span>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import { Doc, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
|
||||
import notification, { ChatMessage } from '@hcengineering/notification'
|
||||
|
||||
export let object: Doc
|
||||
export let chatMessage: ChatMessage | undefined = undefined
|
||||
export let shouldSaveDraft: boolean = true
|
||||
export let focusIndex: number = -1
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
|
||||
type MessageDraft = Pick<ChatMessage, '_id' | 'message' | 'attachments'>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
const _class = notification.class.ChatMessage
|
||||
const createdMessageQuery = createQuery()
|
||||
const account = getCurrentAccount()
|
||||
|
||||
const draftKey = `${object._id}_${_class}`
|
||||
const draftController = new DraftController<MessageDraft>(draftKey)
|
||||
const currentDraft = shouldSaveDraft ? $draftsStore[draftKey] : undefined
|
||||
|
||||
const emptyMessage = {
|
||||
message: '<p></p>',
|
||||
attachments: 0
|
||||
}
|
||||
|
||||
let inputRef: AttachmentRefInput
|
||||
let currentMessage: MessageDraft = chatMessage ?? currentDraft ?? getDefault()
|
||||
let _id = currentMessage._id
|
||||
let inputContent = currentMessage.message
|
||||
let loading = false
|
||||
|
||||
$: createdMessageQuery.query(notification.class.ChatMessage, { _id }, (result: ChatMessage[]) => {
|
||||
if (result.length > 0 && _id !== chatMessage?._id) {
|
||||
// Ouch we have got comment with same id created already.
|
||||
currentMessage = getDefault()
|
||||
_id = currentMessage._id
|
||||
inputRef.removeDraft(false)
|
||||
}
|
||||
})
|
||||
|
||||
function objectChange (draft: MessageDraft, empty: Partial<MessageDraft>) {
|
||||
if (shouldSaveDraft) {
|
||||
draftController.save(draft, empty)
|
||||
}
|
||||
}
|
||||
|
||||
$: objectChange(currentMessage, emptyMessage)
|
||||
|
||||
function getDefault (): MessageDraft {
|
||||
return {
|
||||
_id: generateId(),
|
||||
...emptyMessage
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpdate (event: CustomEvent) {
|
||||
if (!shouldSaveDraft) {
|
||||
return
|
||||
}
|
||||
const { message, attachments } = event.detail
|
||||
currentMessage.message = message
|
||||
currentMessage.attachments = attachments
|
||||
}
|
||||
|
||||
async function onMessage (event: CustomEvent) {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
draftController.remove()
|
||||
inputRef.removeDraft(false)
|
||||
|
||||
if (chatMessage) {
|
||||
await editMessage(event)
|
||||
} else {
|
||||
await createMessage(event)
|
||||
}
|
||||
|
||||
// Remove draft from Local Storage
|
||||
currentMessage = getDefault()
|
||||
_id = currentMessage._id
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
dispatch('submit', false)
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function createMessage (event: CustomEvent) {
|
||||
const { message, attachments } = event.detail
|
||||
|
||||
await client.addCollection<Doc, ChatMessage>(
|
||||
_class,
|
||||
object.space,
|
||||
object._id,
|
||||
object._class,
|
||||
'comments',
|
||||
{ message, attachments },
|
||||
_id,
|
||||
Date.now(),
|
||||
account._id
|
||||
)
|
||||
}
|
||||
|
||||
async function editMessage (event: CustomEvent) {
|
||||
if (chatMessage === undefined) {
|
||||
return
|
||||
}
|
||||
const { message, attachments } = event.detail
|
||||
await client.update(chatMessage, { message, attachments, isEdited: true })
|
||||
}
|
||||
export function submit (): void {
|
||||
inputRef.submit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<AttachmentRefInput
|
||||
{focusIndex}
|
||||
bind:this={inputRef}
|
||||
bind:content={inputContent}
|
||||
{_class}
|
||||
space={object.space}
|
||||
bind:objectId={_id}
|
||||
{shouldSaveDraft}
|
||||
{boundary}
|
||||
on:message={onMessage}
|
||||
on:update={onUpdate}
|
||||
on:focus
|
||||
on:blur
|
||||
bind:loading
|
||||
/>
|
||||
@@ -0,0 +1,143 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import notification, { ChatMessage } from '@hcengineering/notification'
|
||||
import { closeTooltip, Label, Lazy, Spinner, resizeObserver, MiniToggle } from '@hcengineering/ui'
|
||||
import { ObjectPresenter, DocNavLink } from '@hcengineering/view-resources'
|
||||
import ChatMessageInput from './ChatMessageInput.svelte'
|
||||
import ChatMessagePresenter from './ChatMessagePresenter.svelte'
|
||||
|
||||
export let objectId: Ref<Doc>
|
||||
export let object: Doc
|
||||
export let withInput: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
let loading = true
|
||||
let messages: ChatMessage[] = []
|
||||
|
||||
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
|
||||
$: query.query(
|
||||
notification.class.ChatMessage,
|
||||
{ attachedTo: objectId },
|
||||
(res) => {
|
||||
messages = res.sort((message) => (message?.isPinned ? -1 : 1))
|
||||
loading = false
|
||||
},
|
||||
{ sort: { createdOn: activityOrderNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
let isTextMode = false
|
||||
|
||||
$: if (isTextMode) {
|
||||
dispatch('tooltip', { kind: 'popup' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="commentPopup-container">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex-between header"
|
||||
use:resizeObserver={() => {
|
||||
dispatch('changeContent')
|
||||
}}
|
||||
on:keydown={(evt) => {
|
||||
console.log(evt)
|
||||
if (isTextMode) {
|
||||
evt.preventDefault()
|
||||
evt.stopImmediatePropagation()
|
||||
closeTooltip()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="fs-title mr-2">
|
||||
<Label label={notification.string.Comments} />
|
||||
</div>
|
||||
<MiniToggle bind:on={activityOrderNewestFirst} label={notification.string.NewestFirst} />
|
||||
<DocNavLink {object}>
|
||||
<ObjectPresenter _class={object._class} objectId={object._id} value={object} />
|
||||
</DocNavLink>
|
||||
</div>
|
||||
<div class="messages">
|
||||
{#if loading}
|
||||
<div class="flex-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
{#each messages as message}
|
||||
<div class="item">
|
||||
<Lazy>
|
||||
<ChatMessagePresenter value={message} />
|
||||
</Lazy>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if withInput}
|
||||
<div class="input">
|
||||
<ChatMessageInput
|
||||
{object}
|
||||
on:focus={() => {
|
||||
isTextMode = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.commentPopup-container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-height: 30rem;
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
margin: 0 0.25rem 0.5rem;
|
||||
padding: 0.5rem 1.25rem 1rem 0.75rem;
|
||||
border-bottom: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
|
||||
.messages {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 0 1rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
.item {
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
.item + .item {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.5rem 0.25rem 0.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Account, Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import notification, { ChatMessage, ChatMessageViewlet, DisplayActivityMessage } from '@hcengineering/notification'
|
||||
import core from '@hcengineering/core/lib/component'
|
||||
import { AttachmentDocList } from '@hcengineering/attachment-resources'
|
||||
import { LinkPresenter } from '@hcengineering/view-resources'
|
||||
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
|
||||
import { getNotificationObject } from '../../activityMessagesUtils'
|
||||
import ChatMessageHeader from './ChatMessageHeader.svelte'
|
||||
import ChatMessageInput from './ChatMessageInput.svelte'
|
||||
|
||||
export let value: ChatMessage | undefined
|
||||
export let hasNotifyActions = false
|
||||
export let showNotify: boolean = false
|
||||
export let isHighlighted: boolean = false
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const viewletQuery = createQuery()
|
||||
const userQuery = createQuery()
|
||||
|
||||
const currentAccount = getCurrentAccount()
|
||||
|
||||
let user: PersonAccount | undefined = undefined
|
||||
let person: Person | undefined = undefined
|
||||
|
||||
let parentMessage: DisplayActivityMessage | undefined = undefined
|
||||
let parentObject: Doc | undefined
|
||||
let object: Doc | undefined
|
||||
|
||||
let viewlet: ChatMessageViewlet | undefined
|
||||
|
||||
$: value &&
|
||||
viewletQuery.query(
|
||||
notification.class.ChatMessageViewlet,
|
||||
{ objectClass: value.attachedToClass },
|
||||
(result: ChatMessageViewlet[]) => {
|
||||
viewlet = result[0]
|
||||
}
|
||||
)
|
||||
|
||||
$: value &&
|
||||
userQuery.query(core.class.Account, { _id: value.createdBy }, (res: Account[]) => {
|
||||
user = res[0] as PersonAccount
|
||||
})
|
||||
|
||||
$: person = user?.person && $personByIdStore.get(user.person)
|
||||
|
||||
$: value &&
|
||||
getParentMessage(value.attachedToClass, value.attachedTo).then((res) => {
|
||||
parentMessage = res as DisplayActivityMessage
|
||||
})
|
||||
|
||||
$: value &&
|
||||
getNotificationObject(client, value.attachedTo, value.attachedToClass).then((result) => {
|
||||
object = result.object
|
||||
})
|
||||
|
||||
$: parentMessage &&
|
||||
getNotificationObject(client, parentMessage.attachedTo, parentMessage.attachedToClass).then((result) => {
|
||||
parentObject = result.object
|
||||
})
|
||||
|
||||
$: links = getLinks(value?.message)
|
||||
|
||||
function getLinks (content?: string): HTMLLinkElement[] {
|
||||
if (!content) {
|
||||
return []
|
||||
}
|
||||
const parser = new DOMParser()
|
||||
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
|
||||
return parseLinks(parent.childNodes)
|
||||
}
|
||||
|
||||
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
|
||||
const res: HTMLLinkElement[] = []
|
||||
nodes.forEach((node) => {
|
||||
if (node.nodeType !== Node.TEXT_NODE) {
|
||||
if (node.nodeName === 'A') {
|
||||
res.push(node as HTMLLinkElement)
|
||||
}
|
||||
res.push(...parseLinks(node.childNodes))
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
async function getParentMessage (_class: Ref<Class<Doc>>, _id: Ref<Doc>) {
|
||||
if (hierarchy.isDerived(_class, notification.class.ActivityMessage)) {
|
||||
return await client.findOne(_class, { _id })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditAction () {
|
||||
isEditing = true
|
||||
}
|
||||
|
||||
let isEditing = false
|
||||
let actions: Action[] = []
|
||||
|
||||
$: isOwn = user !== undefined && user._id === currentAccount._id
|
||||
|
||||
$: actions = [
|
||||
...(isOwn
|
||||
? [
|
||||
{
|
||||
label: notification.string.Edit,
|
||||
icon: IconEdit,
|
||||
action: handleEditAction
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
|
||||
$: excludedActions = isOwn ? [] : [notification.action.DeleteChatMessage]
|
||||
let refInput: ChatMessageInput
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<ActivityMessageTemplate
|
||||
message={value}
|
||||
{viewlet}
|
||||
{parentMessage}
|
||||
{person}
|
||||
{excludedActions}
|
||||
{showNotify}
|
||||
{isHighlighted}
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{actions}
|
||||
{hasNotifyActions}
|
||||
{onClick}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<ChatMessageHeader {object} {parentObject} message={value} {viewlet} {person} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if !isEditing}
|
||||
<ShowMore>
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={value.message} />
|
||||
<AttachmentDocList {value} />
|
||||
{#each links as link}
|
||||
<LinkPresenter {link} />
|
||||
{/each}
|
||||
</div>
|
||||
</ShowMore>
|
||||
{:else if object}
|
||||
<ChatMessageInput
|
||||
bind:this={refInput}
|
||||
chatMessage={value}
|
||||
{object}
|
||||
on:submit={() => {
|
||||
isEditing = false
|
||||
}}
|
||||
/>
|
||||
<div class="flex-row-center gap-2 justify-end mt-2">
|
||||
<Button
|
||||
label={view.string.Cancel}
|
||||
on:click={() => {
|
||||
isEditing = false
|
||||
}}
|
||||
/>
|
||||
<Button label={notification.string.Update} accent on:click={() => refInput.submit()} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ActivityMessageTemplate>
|
||||
{/if}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<!--
|
||||
// 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 } from '@hcengineering/core'
|
||||
import { Button, ButtonKind, ButtonSize, IconThread } from '@hcengineering/ui'
|
||||
|
||||
import ChatMessagePopup from './ChatMessagePopup.svelte'
|
||||
|
||||
export let value: number | undefined
|
||||
export let object: Doc
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let showCounter: boolean = true
|
||||
export let compactMode: boolean = false
|
||||
export let withInput: boolean = true
|
||||
</script>
|
||||
|
||||
{#if value && value > 0}
|
||||
<Button
|
||||
{kind}
|
||||
{size}
|
||||
showTooltip={{
|
||||
component: ChatMessagePopup,
|
||||
props: { objectId: object._id, object, withInput }
|
||||
}}
|
||||
>
|
||||
<div slot="icon">
|
||||
<IconThread {size} />
|
||||
</div>
|
||||
<div slot="content" style:margin-left={showCounter && !compactMode ? '.375rem' : '0'}>
|
||||
{#if showCounter && !compactMode}{value}{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 notification, { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/notification'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
import AddedAttributesPresenter from './attributes/AddedAttributesPresenter.svelte'
|
||||
import RemovedAttributesPresenter from './attributes/RemovedAttributesPresenter.svelte'
|
||||
import SetAttributesPresenter from './attributes/SetAttributesPresenter.svelte'
|
||||
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let attributeUpdates: DocAttributeUpdates
|
||||
export let attributeModel: AttributeModel
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: presenter =
|
||||
viewlet?.config?.[attributeModel.key]?.presenter ??
|
||||
hierarchy.classHierarchyMixin(attributeUpdates.attrClass, notification.mixin.NotificationAttributePresenter)
|
||||
?.presenter
|
||||
</script>
|
||||
|
||||
{#if presenter}
|
||||
<Component is={presenter} props={{ value: attributeUpdates }} />
|
||||
{:else}
|
||||
{#if attributeUpdates.added.length}
|
||||
<AddedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.added} />
|
||||
{/if}
|
||||
{#if attributeUpdates.removed.length}
|
||||
<RemovedAttributesPresenter {viewlet} {attributeModel} values={attributeUpdates.removed} />
|
||||
{/if}
|
||||
{#if attributeUpdates.set.length}
|
||||
<SetAttributesPresenter
|
||||
{viewlet}
|
||||
{attributeModel}
|
||||
values={attributeUpdates.set}
|
||||
prevValue={attributeUpdates.prevValue}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { AttachedDoc, Attribute, Class, Collection, Doc, Ref } from '@hcengineering/core'
|
||||
import { Icon, Label } from '@hcengineering/ui'
|
||||
import notification, {
|
||||
DocUpdateMessageViewlet,
|
||||
DisplayDocUpdateMessage,
|
||||
DocUpdateMessage
|
||||
} from '@hcengineering/notification'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import NotificationObjectValue from './DocUpdateMessageObjectValue.svelte'
|
||||
|
||||
export let message: DisplayDocUpdateMessage
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let objectName: IntlString
|
||||
export let collectionName: IntlString | undefined
|
||||
export let objectClass: Ref<Class<Doc>>
|
||||
export let collectionAttribute: Attribute<Collection<AttachedDoc>> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const clazz = hierarchy.getClass(message.objectClass)
|
||||
|
||||
const objectPanel = hierarchy.classHierarchyMixin(objectClass, view.mixin.ObjectPanel)
|
||||
const objectPresenter = hierarchy.classHierarchyMixin(objectClass, view.mixin.ObjectPresenter)
|
||||
|
||||
const isOwn = message.objectId === message.attachedTo
|
||||
|
||||
let valueMessages: DocUpdateMessage[] = []
|
||||
|
||||
$: valueMessages = message.previousMessages?.length ? [...message.previousMessages, message] : [message]
|
||||
$: hasDifferentActions = message.previousMessages?.some(({ action }) => action !== message.action)
|
||||
$: icon = viewlet?.icon ?? collectionAttribute?.icon ?? clazz.icon ?? notification.icon.Notifications
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<span class="mr-1">
|
||||
<Icon {icon} size="small" />
|
||||
</span>
|
||||
{#if hasDifferentActions}
|
||||
<Label label={notification.string.UpdatedCollection} />
|
||||
{:else if message.action === 'create'}
|
||||
<Label label={notification.string.New} />
|
||||
{:else if message.action === 'remove' && message.updateCollection}
|
||||
<Label label={notification.string.Removed} />
|
||||
{/if}
|
||||
<span class="lower">
|
||||
{#if collectionName && (message.previousMessages?.length || !isOwn)}
|
||||
<Label label={collectionName} />:
|
||||
{:else}
|
||||
<Label label={objectName} />:
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if hasDifferentActions}
|
||||
{@const removeMessages = valueMessages.filter(({ action }) => action === 'remove')}
|
||||
{@const createMessages = valueMessages.filter(({ action }) => action === 'create')}
|
||||
|
||||
{#each createMessages as valueMessage, index}
|
||||
<NotificationObjectValue
|
||||
message={valueMessage}
|
||||
{objectPresenter}
|
||||
{objectPanel}
|
||||
{viewlet}
|
||||
withIcon={index === 0}
|
||||
hasSeparator={createMessages.length > 1 && index !== createMessages.length - 1}
|
||||
/>
|
||||
{/each}
|
||||
{#each removeMessages as valueMessage, index}
|
||||
<NotificationObjectValue
|
||||
message={valueMessage}
|
||||
{objectPresenter}
|
||||
{objectPanel}
|
||||
{viewlet}
|
||||
withIcon={index === 0}
|
||||
hasSeparator={removeMessages.length > 1 && index !== removeMessages.length - 1}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each valueMessages as valueMessage, index}
|
||||
<NotificationObjectValue
|
||||
message={valueMessage}
|
||||
{objectPresenter}
|
||||
{objectPanel}
|
||||
{viewlet}
|
||||
hasSeparator={valueMessages.length > 1 && index !== valueMessages.length - 1}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { DocNavLink } from '@hcengineering/view-resources'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import notification, { DocUpdateMessageViewlet, DisplayDocUpdateMessage } from '@hcengineering/notification'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
|
||||
import { LinkData, getLinkData } from '../../activityMessagesUtils'
|
||||
|
||||
export let message: DisplayDocUpdateMessage
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let person: Person | undefined
|
||||
export let objectName: IntlString | undefined = undefined
|
||||
export let object: Doc | undefined
|
||||
export let parentObject: Doc | undefined
|
||||
export let attributeModel: AttributeModel | undefined = undefined
|
||||
export let collectionName: IntlString | undefined = undefined
|
||||
|
||||
const isOwn = message.objectId === message.attachedTo
|
||||
|
||||
let linkData: LinkData | undefined = undefined
|
||||
|
||||
$: getLinkData(message, object, parentObject, person).then((data) => {
|
||||
linkData = data
|
||||
})
|
||||
|
||||
function getTitle (): IntlString {
|
||||
const { attributeUpdates } = message
|
||||
const added = attributeUpdates?.added ?? []
|
||||
const removed = attributeUpdates?.removed ?? []
|
||||
|
||||
if (added.length > 0 && removed.length === 0) {
|
||||
return notification.string.Added
|
||||
}
|
||||
if (removed.length > 0 && added.length === 0) {
|
||||
return notification.string.Removed
|
||||
}
|
||||
|
||||
return notification.string.Changed
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if viewlet?.label}
|
||||
<span class="text-sm lower"> <Label label={viewlet.label} /></span>
|
||||
{:else if message.previousMessages?.some(({ action }) => action !== message.action)}
|
||||
{@const name = collectionName ?? objectName}
|
||||
<span class="text-sm lower">
|
||||
<Label label={notification.string.Updated} />
|
||||
{#if name}
|
||||
<Label label={name} />
|
||||
{/if}
|
||||
</span>
|
||||
{:else if message.action === 'create' && objectName}
|
||||
<span class="text-sm lower">
|
||||
<Label label={isOwn ? notification.string.Created : notification.string.Added} />
|
||||
{#if collectionName && (!isOwn || message.previousMessages?.length)}
|
||||
<Label label={collectionName} />
|
||||
{:else}
|
||||
<Label label={objectName} />
|
||||
{/if}
|
||||
</span>
|
||||
{:else if message.action === 'remove' && objectName}
|
||||
<span class="text-sm lower">
|
||||
<Label label={notification.string.Removed} />
|
||||
{#if collectionName && message.previousMessages?.length}
|
||||
<Label label={collectionName} />
|
||||
{:else}
|
||||
<Label label={objectName} />
|
||||
{/if}
|
||||
</span>
|
||||
{:else if attributeModel}
|
||||
<span class="text-sm lower"><Label label={getTitle()} /></span>
|
||||
<span class="text-sm lower"> <Label label={attributeModel.label} /></span>
|
||||
{/if}
|
||||
|
||||
{#if linkData}
|
||||
{#if !isOwn || message.action === 'update'}
|
||||
<span class="text-sm lower"><Label label={linkData.preposition} /></span>
|
||||
{/if}
|
||||
<span class="text-sm">
|
||||
<DocNavLink object={linkData.object} component={linkData.panelComponent} shrink={0}>
|
||||
<span class="overflow-label select-text">{linkData.title}</span>
|
||||
</DocNavLink>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { DocNavLink, getDocLinkTitle } from '@hcengineering/view-resources'
|
||||
import { Component, Icon, IconAdd, IconDelete } from '@hcengineering/ui'
|
||||
import { DocUpdateMessageViewlet, DisplayDocUpdateMessage } from '@hcengineering/notification'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import view, { ObjectPanel, ObjectPresenter } from '@hcengineering/view'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
|
||||
import { getNotificationObject } from '../../activityMessagesUtils'
|
||||
|
||||
export let message: DisplayDocUpdateMessage
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let objectPanel: ObjectPanel | undefined
|
||||
export let objectPresenter: ObjectPresenter | undefined
|
||||
export let withIcon: boolean = false
|
||||
export let hasSeparator: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function getValue (object: Doc | undefined): Promise<string | undefined> {
|
||||
if (object === undefined) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (viewlet?.valueAttr) {
|
||||
return (object as any)[viewlet.valueAttr]
|
||||
}
|
||||
|
||||
return await getDocLinkTitle(client, object._id, object._class, object)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await getNotificationObject(client, message.objectId, message.objectClass) then { object }}
|
||||
{#await getValue(object) then value}
|
||||
{#if withIcon && message.action === 'create'}
|
||||
<Icon icon={IconAdd} size="x-small" />
|
||||
{/if}
|
||||
{#if withIcon && message.action === 'remove'}
|
||||
<Icon icon={IconDelete} size="x-small" />
|
||||
{/if}
|
||||
|
||||
{#if value}
|
||||
<span>
|
||||
<DocNavLink
|
||||
{object}
|
||||
disabled={message.action === 'remove'}
|
||||
component={objectPanel?.component ?? view.component.EditDoc}
|
||||
shrink={0}
|
||||
>
|
||||
<span class="overflow-label select-text valueLink">{value}</span>
|
||||
</DocNavLink>
|
||||
{#if hasSeparator}
|
||||
<span class="separator">,</span>
|
||||
{/if}
|
||||
</span>
|
||||
{:else if objectPresenter && object}
|
||||
<Component is={objectPresenter.presenter} props={{ value: object, accent: true, shouldShowAvatar: false }} />
|
||||
{/if}
|
||||
{/await}
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.valueLink {
|
||||
font-weight: 500;
|
||||
color: var(--theme-link-color);
|
||||
}
|
||||
|
||||
.separator {
|
||||
font-weight: 500;
|
||||
color: var(--theme-link-color);
|
||||
margin-left: -0.25rem;
|
||||
}
|
||||
</style>
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Account, AttachedDoc, Class, Collection, Doc, Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import notification, {
|
||||
ActivityMessage,
|
||||
DocUpdateMessageViewlet,
|
||||
DocUpdateMessage,
|
||||
DisplayDocUpdateMessage,
|
||||
DisplayActivityMessage,
|
||||
DocUpdateAction
|
||||
} from '@hcengineering/notification'
|
||||
import core from '@hcengineering/core/lib/component'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Component, ShowMore } from '@hcengineering/ui'
|
||||
|
||||
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
|
||||
import DocUpdateMessageHeader from './DocUpdateMessageHeader.svelte'
|
||||
import DocUpdateMessageContent from './DocUpdateMessageContent.svelte'
|
||||
import DocUpdateMessageAttributes from './DocUpdateMessageAttributes.svelte'
|
||||
|
||||
import { getAttributeModel, getCollectionAttribute, getNotificationObject } from '../../activityMessagesUtils'
|
||||
|
||||
export let value: DisplayDocUpdateMessage
|
||||
export let hasNotifyActions = false
|
||||
export let showNotify: boolean = false
|
||||
export let isHighlighted: boolean = false
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let hasActionsMenu: boolean = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const viewletQuery = createQuery()
|
||||
const userQuery = createQuery()
|
||||
|
||||
const collectionAttribute = getCollectionAttribute(hierarchy, value.attachedToClass, value.updateCollection)
|
||||
const clazz = hierarchy.getClass(value.objectClass)
|
||||
const objectName: IntlString | undefined =
|
||||
(collectionAttribute?.type as Collection<AttachedDoc>)?.itemLabel || clazz.label
|
||||
const collectionName = collectionAttribute?.label
|
||||
|
||||
let user: PersonAccount | undefined = undefined
|
||||
let person: Person | undefined = undefined
|
||||
let viewlet: DocUpdateMessageViewlet | undefined
|
||||
let attributeModel: AttributeModel | undefined = undefined
|
||||
let parentMessage: DisplayActivityMessage | undefined = undefined
|
||||
let parentObject: Doc | undefined
|
||||
let object: Doc | undefined
|
||||
let isObjectRemoved: boolean = false
|
||||
|
||||
let isViewletLoading = true
|
||||
let isObjectLoading = true
|
||||
|
||||
$: isLoading = isViewletLoading || isObjectLoading
|
||||
|
||||
$: loadViewlet(value.action, value.objectClass)
|
||||
|
||||
async function loadViewlet (action: DocUpdateAction, objectClass: Ref<Class<Doc>>) {
|
||||
isViewletLoading = true
|
||||
|
||||
const res = viewletQuery.query(
|
||||
notification.class.DocUpdateMessageViewlet,
|
||||
{ action, objectClass },
|
||||
(result: DocUpdateMessageViewlet[]) => {
|
||||
viewlet = result[0]
|
||||
isViewletLoading = false
|
||||
}
|
||||
)
|
||||
|
||||
if (!res) {
|
||||
isViewletLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: getAttributeModel(client, value.attributeUpdates, value.attachedToClass).then((model) => {
|
||||
attributeModel = model
|
||||
})
|
||||
|
||||
async function getParentMessage (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<ActivityMessage | undefined> {
|
||||
if (hierarchy.isDerived(_class, notification.class.ActivityMessage)) {
|
||||
return await client.findOne(notification.class.ActivityMessage, { _id: _id as Ref<ActivityMessage> })
|
||||
}
|
||||
}
|
||||
|
||||
$: getParentMessage(value.attachedToClass, value.attachedTo).then((res) => {
|
||||
parentMessage = res as DisplayActivityMessage
|
||||
})
|
||||
|
||||
$: userQuery.query(core.class.Account, { _id: value.createdBy }, (res: Account[]) => {
|
||||
user = res[0] as PersonAccount
|
||||
})
|
||||
|
||||
$: person = user?.person && $personByIdStore.get(user.person)
|
||||
|
||||
$: getNotificationObject(client, value.objectId, value.objectClass).then((result) => {
|
||||
isObjectLoading = false
|
||||
object = result.object
|
||||
isObjectRemoved = result.isRemoved
|
||||
})
|
||||
|
||||
$: getParentObject(value, parentMessage).then((result) => {
|
||||
parentObject = result?.object
|
||||
})
|
||||
|
||||
async function getParentObject (message: DocUpdateMessage, parentMessage?: ActivityMessage) {
|
||||
if (parentMessage) {
|
||||
return await getNotificationObject(client, parentMessage.attachedTo, parentMessage.attachedToClass)
|
||||
}
|
||||
|
||||
if (message.objectId === message.attachedTo) {
|
||||
return
|
||||
}
|
||||
|
||||
return await getNotificationObject(client, message.attachedTo, message.attachedToClass)
|
||||
}
|
||||
|
||||
$: if (object && value.objectClass !== object._class) {
|
||||
object = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !isLoading && (!viewlet?.hideIfRemoved || !isObjectRemoved) && (value.action !== 'update' || attributeModel !== undefined)}
|
||||
<ActivityMessageTemplate
|
||||
message={value}
|
||||
{parentMessage}
|
||||
{person}
|
||||
{showNotify}
|
||||
{isHighlighted}
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{hasActionsMenu}
|
||||
{viewlet}
|
||||
{hasNotifyActions}
|
||||
{onClick}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
{#if viewlet?.labelComponent}
|
||||
<Component is={viewlet.labelComponent} props={{ value: object }} />
|
||||
{:else}
|
||||
<DocUpdateMessageHeader
|
||||
message={value}
|
||||
{object}
|
||||
{parentObject}
|
||||
{viewlet}
|
||||
{person}
|
||||
{objectName}
|
||||
{collectionName}
|
||||
{attributeModel}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if viewlet?.component}
|
||||
<ShowMore>
|
||||
<div class="customContent">
|
||||
{#each [...(value?.previousMessages ?? []), value] as msg}
|
||||
{#await getNotificationObject(client, msg.objectId, msg.objectClass) then { object }}
|
||||
{#if object}
|
||||
<Component is={viewlet.component} props={{ message: value, value: object }} />
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
</div>
|
||||
</ShowMore>
|
||||
{:else if value.action === 'create' || value.action === 'remove'}
|
||||
<ShowMore>
|
||||
<DocUpdateMessageContent
|
||||
objectClass={value.objectClass}
|
||||
message={value}
|
||||
{viewlet}
|
||||
{objectName}
|
||||
{collectionName}
|
||||
{collectionAttribute}
|
||||
/>
|
||||
</ShowMore>
|
||||
{:else if value.attributeUpdates && attributeModel}
|
||||
<DocUpdateMessageAttributes attributeUpdates={value.attributeUpdates} {attributeModel} {viewlet} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ActivityMessageTemplate>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.customContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 0.625rem;
|
||||
row-gap: 0.625rem;
|
||||
}
|
||||
</style>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 notification, { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/notification'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import ChangeAttributesTemplate from './ChangeAttributesTemplate.svelte'
|
||||
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let attributeModel: AttributeModel
|
||||
export let values: DocAttributeUpdates['added']
|
||||
</script>
|
||||
|
||||
<ChangeAttributesTemplate {viewlet} {attributeModel} {values}>
|
||||
<svelte:fragment slot="text">
|
||||
<Label label={notification.string.New} />
|
||||
<span class="lower"><Label label={attributeModel.label} />:</span>
|
||||
</svelte:fragment>
|
||||
</ChangeAttributesTemplate>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/notification'
|
||||
import { Icon, IconEdit, Component } from '@hcengineering/ui'
|
||||
import { ObjectPresenter } from '@hcengineering/view-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
|
||||
import { getAttributeValues } from '../../../activityMessagesUtils'
|
||||
|
||||
type Values = DocAttributeUpdates['set'] | DocAttributeUpdates['added'] | DocAttributeUpdates['removed']
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let attributeModel: AttributeModel
|
||||
export let values: Values
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let attributeValues: Values | Doc[] = []
|
||||
|
||||
$: getAttributeValues(client, values, attributeModel._class).then((result) => {
|
||||
attributeValues = result
|
||||
})
|
||||
|
||||
$: attrViewletConfig = viewlet?.config?.[attributeModel.key]
|
||||
$: attributeIcon = attrViewletConfig?.icon ?? attributeModel.icon ?? IconEdit
|
||||
$: space = typeof attributeValues[0] === 'object' ? attributeValues[0]?.space : undefined
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<span class="mr-1">
|
||||
{#if attrViewletConfig?.iconPresenter}
|
||||
<Component is={attrViewletConfig?.iconPresenter} props={{ value: attributeValues[0], space, size: 'small' }} />
|
||||
{:else}
|
||||
<Icon icon={attributeIcon} size="small" />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<slot name="text" />
|
||||
|
||||
{#each attributeValues as value}
|
||||
<span class="strong">
|
||||
{#if value !== null && typeof value === 'object'}
|
||||
<ObjectPresenter {value} shouldShowAvatar={false} accent />
|
||||
{:else}
|
||||
<svelte:component this={attributeModel.presenter} {value} shouldShowAvatar={false} accent kind="list-header" />
|
||||
{/if}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 notification, { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/notification'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
|
||||
import ChangeAttributesTemplate from './ChangeAttributesTemplate.svelte'
|
||||
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let attributeModel: AttributeModel
|
||||
export let values: DocAttributeUpdates['removed']
|
||||
</script>
|
||||
|
||||
<ChangeAttributesTemplate {viewlet} {attributeModel} {values}>
|
||||
<svelte:fragment slot="text">
|
||||
<Label label={notification.string.Removed} />
|
||||
<span class="lower"> <Label label={attributeModel.label} />:</span>
|
||||
</svelte:fragment>
|
||||
</ChangeAttributesTemplate>
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 notification, { DocAttributeUpdates, DocUpdateMessageViewlet } from '@hcengineering/notification'
|
||||
import ui, { Icon, Label, IconEdit } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import core from '@hcengineering/core/lib/component'
|
||||
|
||||
import ChangeAttributesTemplate from './ChangeAttributesTemplate.svelte'
|
||||
|
||||
export let viewlet: DocUpdateMessageViewlet | undefined
|
||||
export let attributeModel: AttributeModel
|
||||
export let values: DocAttributeUpdates['set']
|
||||
export let prevValue: any
|
||||
|
||||
$: attrViewletConfig = viewlet?.config?.[attributeModel.key]
|
||||
$: attributeIcon = attrViewletConfig?.icon ?? attributeModel.icon ?? IconEdit
|
||||
$: isUnset = values.length > 0 && !values.some((value) => value !== null && value !== '')
|
||||
|
||||
$: isTextType = getIsTextType(attributeModel)
|
||||
|
||||
function getIsTextType (attributeModel: AttributeModel): boolean {
|
||||
return attributeModel.attribute?.type?._class === core.class.TypeMarkup
|
||||
}
|
||||
|
||||
let isDiffShown = false
|
||||
|
||||
function toggleShowMore () {
|
||||
isDiffShown = !isDiffShown
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isUnset}
|
||||
<div class="unset">
|
||||
<span class="mr-1"><Icon icon={attributeIcon} size="small" /></span>
|
||||
<Label label={notification.string.Unset} />
|
||||
<span class="lower"><Label label={attributeModel.label} /></span>
|
||||
</div>
|
||||
{:else if isTextType}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="showMore" on:click={toggleShowMore}>
|
||||
<div class="triangle" class:left={!isDiffShown} class:down={isDiffShown} />
|
||||
<Label label={isDiffShown ? ui.string.ShowLess : ui.string.ShowMore} />
|
||||
</div>
|
||||
{#if isDiffShown}
|
||||
<svelte:component this={attributeModel.presenter} value={values[0]} {prevValue} showOnlyDiff />
|
||||
{/if}
|
||||
{:else}
|
||||
<ChangeAttributesTemplate {viewlet} {attributeModel} {values}>
|
||||
<svelte:fragment slot="text">
|
||||
<Label label={attributeModel.label} />
|
||||
<span class="lower"><Label label={notification.string.Set} /></span>
|
||||
<span class="lower"><Label label={notification.string.To} /></span>
|
||||
</svelte:fragment>
|
||||
</ChangeAttributesTemplate>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.unset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.showMore {
|
||||
color: var(--theme-link-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
gap: 0.5rem;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&.left {
|
||||
border-top: 0.25rem solid transparent;
|
||||
border-bottom: 0.25rem solid transparent;
|
||||
border-left: 0.25rem solid var(--theme-link-color);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.down {
|
||||
border-left: 0.25rem solid transparent;
|
||||
border-right: 0.25rem solid transparent;
|
||||
border-top: 0.25rem solid var(--theme-link-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-toggle-on-bg-hover);
|
||||
|
||||
.triangle {
|
||||
border-left-color: var(--theme-toggle-on-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
// Copyright © 2023 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: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM7.15467 12.4273C8.66416 13.9463 11.0877 14.0045 12.6671 12.5961L12.8453 12.4273C13.04 12.2314 13.3566 12.2304 13.5524 12.4251C13.7265 12.5981 13.7467 12.8674 13.6123 13.0627L13.5547 13.1322L13.5323 13.1545C11.5691 15.1054 8.39616 15.0953 6.44533 13.1322C6.25069 12.9363 6.25169 12.6197 6.44757 12.4251C6.64344 12.2304 6.96002 12.2314 7.15467 12.4273ZM12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5ZM7.5 7.5C8.05228 7.5 8.5 7.94772 8.5 8.5C8.5 9.05228 8.05228 9.5 7.5 9.5C6.94772 9.5 6.5 9.05228 6.5 8.5C6.5 7.94772 6.94772 7.5 7.5 7.5Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
// Copyright © 2023 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: '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="M13.1,2.9C11.8,1.5,9.9,0.7,8,0.7c-1.9,0-3.8,0.8-5.1,2.1C0.7,5,0.1,8.3,1.4,11c0.1,0.3,0.2,0.5,0.2,0.7 c0,0.2-0.1,0.5-0.2,0.8c-0.2,0.6-0.5,1.4,0.1,1.9c0.6,0.6,1.4,0.3,1.9,0.1c0.3-0.1,0.6-0.2,0.8-0.2c0.2,0,0.4,0.1,0.7,0.2 c1,0.4,2,0.7,3,0.7c1.9,0,3.8-0.7,5.2-2.1h0C16,10.3,16,5.7,13.1,2.9z M12.3,12.3c-1.8,1.8-4.5,2.3-6.9,1.2 c-0.4-0.2-0.8-0.3-1.2-0.3c-0.4,0-0.8,0.1-1.2,0.3c-0.2,0.1-0.6,0.2-0.7,0.2c0-0.1,0.1-0.5,0.2-0.7c0.1-0.4,0.3-0.8,0.3-1.2 c0-0.4-0.2-0.8-0.3-1.2C1.4,8.3,1.9,5.5,3.7,3.7C4.9,2.6,6.4,1.9,8,1.9s3.1,0.6,4.3,1.8C14.7,6.1,14.7,9.9,12.3,12.3L12.3,12.3z"
|
||||
/>
|
||||
<path d="M10.6,7.5L10.6,7.5c-0.4,0-0.8,0.3-0.8,0.8S10.2,9,10.6,9s0.8-0.3,0.8-0.8S11,7.5,10.6,7.5z" />
|
||||
<path d="M8,7.5L8,7.5c-0.4,0-0.8,0.3-0.8,0.8S7.5,9,8,9s0.8-0.3,0.8-0.8S8.4,7.5,8,7.5z" />
|
||||
<path d="M5.3,7.5L5.3,7.5c-0.4,0-0.8,0.3-0.8,0.8S4.9,9,5.3,9S6,8.7,6,8.3S5.7,7.5,5.3,7.5z" />
|
||||
</svg>
|
||||
@@ -0,0 +1,141 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 {
|
||||
DisplayActivityMessage,
|
||||
InboxNotification,
|
||||
DocNotifyContext,
|
||||
ActivityMessage,
|
||||
DisplayDocUpdateMessage
|
||||
} from '@hcengineering/notification'
|
||||
import { ActionContext, getClient } from '@hcengineering/presentation'
|
||||
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { getLocation, Label, location, navigate, Scroller } from '@hcengineering/ui'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import ActivityMessagePresenter from '../activity-message/ActivityMessagePresenter.svelte'
|
||||
import notification from '../../plugin'
|
||||
import { getDisplayActivityMessagesByNotifications } from '../../activityMessagesUtils'
|
||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||
import Filter from '../Filter.svelte'
|
||||
|
||||
export let label: IntlString
|
||||
export let _class: Ref<Class<Doc>> | undefined = undefined
|
||||
|
||||
let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||
let filter: 'all' | 'read' | 'unread' = 'all'
|
||||
|
||||
const notificationsClient = InboxNotificationsClientImpl.getClient()
|
||||
const client = getClient()
|
||||
|
||||
let docNotifyContextById: Map<Ref<DocNotifyContext>, DocNotifyContext> = new Map<
|
||||
Ref<DocNotifyContext>,
|
||||
DocNotifyContext
|
||||
>()
|
||||
let inboxNotificationByMessage: Map<Ref<ActivityMessage>, InboxNotification> = new Map<
|
||||
Ref<ActivityMessage>,
|
||||
InboxNotification
|
||||
>()
|
||||
let inboxNotifications: WithLookup<InboxNotification>[] = []
|
||||
|
||||
notificationsClient.docNotifyContexts.subscribe((res: DocNotifyContext[]) => {
|
||||
docNotifyContextById = new Map(res.map((update) => [update._id, update]))
|
||||
})
|
||||
|
||||
notificationsClient.inboxNotifications.subscribe((res: InboxNotification[]) => {
|
||||
inboxNotifications = res
|
||||
inboxNotificationByMessage = new Map(res.map((notification) => [notification.attachedTo, notification]))
|
||||
})
|
||||
|
||||
function markNotificationAsViewed (message: DisplayActivityMessage) {
|
||||
const combinedIds =
|
||||
message._class === notification.class.DocUpdateMessage
|
||||
? (message as DisplayDocUpdateMessage).combinedMessagesIds
|
||||
: undefined
|
||||
const allMessagesIds = [message._id, ...(combinedIds ?? [])]
|
||||
const notifications = allMessagesIds
|
||||
.map((id) => inboxNotificationByMessage.get(id))
|
||||
.filter((n): n is InboxNotification => !!n)
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
client.update(notification, { isViewed: true })
|
||||
})
|
||||
}
|
||||
|
||||
location.subscribe((loc) => {
|
||||
selectedMessageId = loc.path[4] as Ref<ActivityMessage> | undefined
|
||||
})
|
||||
|
||||
function openDocActivity (_id: Ref<Doc>) {
|
||||
const loc = getLocation()
|
||||
loc.path[4] = _id
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
function handleMessageClicked (message: DisplayActivityMessage) {
|
||||
if (client.getHierarchy().isDerived(message.attachedToClass, notification.class.ActivityMessage)) {
|
||||
openDocActivity(message.attachedTo)
|
||||
selectedMessageId = message.attachedTo as Ref<ActivityMessage>
|
||||
} else {
|
||||
openDocActivity(message._id)
|
||||
selectedMessageId = message._id
|
||||
}
|
||||
markNotificationAsViewed(message)
|
||||
}
|
||||
|
||||
$: displayMessages = getDisplayActivityMessagesByNotifications(
|
||||
inboxNotifications,
|
||||
docNotifyContextById,
|
||||
filter,
|
||||
_class
|
||||
)
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="ac-header full divide caption-height">
|
||||
<div class="ac-header__wrap-title mr-3">
|
||||
<span class="ac-header__title"><Label {label} /></span>
|
||||
</div>
|
||||
<div class="flex flex-gap-2">
|
||||
<Filter bind:filter />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Scroller>
|
||||
<div class="content">
|
||||
{#each displayMessages as message}
|
||||
<ActivityMessagePresenter
|
||||
value={message}
|
||||
showNotify={!inboxNotificationByMessage.get(message._id)?.isViewed}
|
||||
hasNotifyActions
|
||||
isSelected={message._id === selectedMessageId}
|
||||
onClick={() => {
|
||||
handleMessageClicked(message)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, IconClose, Spinner } from '@hcengineering/ui'
|
||||
import notification, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/notification'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import ActivityMessagePresenter from '../activity-message/ActivityMessagePresenter.svelte'
|
||||
import { combineActivityMessages, getNotificationObject } from '../../activityMessagesUtils'
|
||||
import ChatMessageInput from '../chat-message/ChatMessageInput.svelte'
|
||||
|
||||
export let _id: Ref<ActivityMessage>
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const dispatch = createEventDispatcher()
|
||||
const selectedMessageQuery = createQuery()
|
||||
const messagesQuery = createQuery()
|
||||
|
||||
let messages: DisplayActivityMessage[] = []
|
||||
let selectedMessage: ActivityMessage | undefined = undefined
|
||||
let object: Doc | undefined = undefined
|
||||
let isLoading: boolean = true
|
||||
|
||||
let scrollElement: HTMLDivElement | undefined
|
||||
|
||||
$: selectedMessageQuery.query(
|
||||
notification.class.ActivityMessage,
|
||||
{ _id },
|
||||
(result: ActivityMessage[]) => {
|
||||
selectedMessage = result[0]
|
||||
},
|
||||
{
|
||||
limit: 1
|
||||
}
|
||||
)
|
||||
|
||||
$: selectedMessage &&
|
||||
getNotificationObject(client, selectedMessage.attachedTo, selectedMessage.attachedToClass).then((res) => {
|
||||
object = res.object
|
||||
})
|
||||
$: objectPresenter =
|
||||
selectedMessage && hierarchy.classHierarchyMixin(selectedMessage.attachedToClass, view.mixin.ObjectPresenter)
|
||||
|
||||
$: selectedMessage &&
|
||||
messagesQuery.query(
|
||||
notification.class.ActivityMessage,
|
||||
{ attachedTo: selectedMessage.attachedTo },
|
||||
(result: ActivityMessage[]) => {
|
||||
messages = combineActivityMessages(result)
|
||||
isLoading = false
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
modifiedOn: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function scrollToBottom () {
|
||||
setTimeout(() => {
|
||||
if (scrollElement !== undefined) {
|
||||
scrollElement.scrollTo(0, scrollElement.scrollHeight)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full divide caption-height withoutBackground">
|
||||
<div class="ac-header__wrap-title mr-3">
|
||||
{#if objectPresenter && object}
|
||||
<Component is={objectPresenter.presenter} props={{ value: object }} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{messages.length}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="tool" on:click={() => dispatch('close')}>
|
||||
<IconClose size="medium" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="spinner">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isLoading}
|
||||
<div class="flex-col vScroll content" bind:this={scrollElement}>
|
||||
{#each messages as message}
|
||||
<ActivityMessagePresenter
|
||||
value={message}
|
||||
isHighlighted={message._id === _id}
|
||||
shouldScroll={_id === message._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if object}
|
||||
<div class="ref-input">
|
||||
<ChatMessageInput {object} on:submit={scrollToBottom} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool {
|
||||
margin-left: 0.75rem;
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ref-input {
|
||||
margin: 1.25rem 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user