mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 21:45:01 +02:00
Merge branch 'main' into ano/attachments-initial
# Conflicts: # plugins/board-resources/src/components/editor/CardActions.svelte
This commit is contained in:
@@ -36,20 +36,18 @@
|
||||
export let _class: Ref<Class<Card>>
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
const cardQuery = createQuery()
|
||||
const stateQuery = createQuery()
|
||||
|
||||
let object: Card | undefined
|
||||
let state: State | undefined
|
||||
let handleMove: () => void
|
||||
|
||||
$: _id &&
|
||||
_class &&
|
||||
query.query(_class, { _id }, async (result) => {
|
||||
$: cardQuery.query(_class, { _id }, async (result) => {
|
||||
object = result[0]
|
||||
})
|
||||
|
||||
$: object &&
|
||||
query.query(task.class.State, { _id: object.state }, async (result) => {
|
||||
$: object?.state && stateQuery.query(task.class.State, { _id: object.state }, async (result) => {
|
||||
state = result[0]
|
||||
})
|
||||
|
||||
@@ -104,7 +102,7 @@
|
||||
<div class="flex-grow mr-4">
|
||||
<div class="flex-row-streach">
|
||||
<div class="w-9" />
|
||||
<CardDetails value={object} />
|
||||
<CardDetails bind:value={object} />
|
||||
</div>
|
||||
<div class="flex-row-streach mt-4 mb-2">
|
||||
<div class="w-9">
|
||||
@@ -128,10 +126,10 @@
|
||||
</div>
|
||||
<CardAttachments value={object} />
|
||||
<!-- TODO checklists -->
|
||||
<CardActivity value={object} />
|
||||
<CardActivity bind:value={object} />
|
||||
</div>
|
||||
|
||||
<CardActions value={object} />
|
||||
<CardActions bind:value={object} />
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
@@ -36,12 +36,12 @@ export function hasDate (card: Card): boolean {
|
||||
|
||||
export function addCurrentUser (card: Card, client: Client): void {
|
||||
const employee = (getCurrentAccount() as EmployeeAccount).employee
|
||||
card.members = card.members ?? []
|
||||
const members = card.members ?? []
|
||||
|
||||
if (card.members.includes(employee)) {
|
||||
if (members.includes(employee)) {
|
||||
return
|
||||
}
|
||||
|
||||
card.members.push(employee)
|
||||
updateCard(client, card, 'members', card.members)
|
||||
members.push(employee)
|
||||
updateCard(client, card, 'members', members)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={participants}
|
||||
title={calendar.string.Participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
participants.push(evt.detail._id)
|
||||
participants = participants
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={object.participants}
|
||||
title={calendar.string.Participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
client.update(object, { $push: { participants: evt.detail._id } })
|
||||
}}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={participants}
|
||||
title={calendar.string.Participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
participants.push(evt.detail._id)
|
||||
participants = participants
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
"MarkUnread": "Mark unread",
|
||||
"GetNewReplies": "Get notified about new replies",
|
||||
"TurnOffReplies": "Turn off notifications for replies",
|
||||
"PinMessage": "Pin message",
|
||||
"UnpinMessage": "Unpin message",
|
||||
"Pinned": "Pinned:",
|
||||
"EditMessage": "Edit message",
|
||||
"DeleteMessage": "Delete message"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,10 @@
|
||||
"MarkUnread": "Отметить как непрочитанное",
|
||||
"GetNewReplies": "Получать уведомления о новых ответах",
|
||||
"TurnOffReplies": "Выключить уведомления об ответах",
|
||||
"PinMessage": "Закрепить сообщение",
|
||||
"UnpinMessage": "Открепить сообщение",
|
||||
"Pinned": "Закреплено:",
|
||||
"EditMessage": "Редактировать сообщение",
|
||||
"DeleteMessage": "Удалить сообщение"
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import type { Message } from '@anticrm/chunter'
|
||||
import type { ChunterMessage, Message } from '@anticrm/chunter'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import core, { Doc, Ref, Space, WithLookup } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
@@ -23,7 +23,9 @@
|
||||
import chunter from '../plugin'
|
||||
import ChannelSeparator from './ChannelSeparator.svelte'
|
||||
import MessageComponent from './Message.svelte'
|
||||
|
||||
export let space: Ref<Space> | undefined
|
||||
export let pinnedIds: Ref<ChunterMessage>[]
|
||||
|
||||
let div: HTMLDivElement | undefined
|
||||
let autoscroll: boolean = false
|
||||
@@ -111,7 +113,7 @@
|
||||
{#if newMessagesPos === i}
|
||||
<ChannelSeparator title={chunter.string.New} line reverse isNew />
|
||||
{/if}
|
||||
<MessageComponent {message} {employees} on:openThread />
|
||||
<MessageComponent {message} {employees} on:openThread isPinned={pinnedIds.includes(message._id)} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentRefInput } from '@anticrm/attachment-resources'
|
||||
import { Message } from '@anticrm/chunter'
|
||||
import { ChunterMessage, Message } from '@anticrm/chunter'
|
||||
import { generateId, getCurrentAccount, Ref, Space, TxFactory } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import { createBacklinks } from '../backlinks'
|
||||
import chunter from '../plugin'
|
||||
import Channel from './Channel.svelte'
|
||||
import PinnedMessages from './PinnedMessages.svelte'
|
||||
|
||||
export let space: Ref<Space>
|
||||
|
||||
@@ -63,13 +64,26 @@
|
||||
loc.path[3] = _id
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
let pinnedIds: Ref<ChunterMessage>[] = []
|
||||
pinnedQuery.query(
|
||||
chunter.class.Channel,
|
||||
{ _id: space },
|
||||
(res) => {
|
||||
pinnedIds = res[0]?.pinned ?? []
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
</script>
|
||||
|
||||
<PinnedMessages {space} {pinnedIds} />
|
||||
<Channel
|
||||
{space}
|
||||
on:openThread={(e) => {
|
||||
openThread(e.detail)
|
||||
}}
|
||||
{pinnedIds}
|
||||
/>
|
||||
<div class="reference">
|
||||
<AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage} />
|
||||
|
||||
@@ -14,17 +14,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Attachment } from '@anticrm/attachment'
|
||||
import { AttachmentList } from '@anticrm/attachment-resources'
|
||||
import { AttachmentList, AttachmentRefInput } from '@anticrm/attachment-resources'
|
||||
import type { Message } from '@anticrm/chunter'
|
||||
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { Avatar, getClient, MessageViewer } from '@anticrm/presentation'
|
||||
import { ActionIcon, IconMoreH, Menu, showPopup, getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import { ActionIcon, IconMoreH, Menu, showPopup } from '@anticrm/ui'
|
||||
import { Action } from '@anticrm/view'
|
||||
import { getActions } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { UnpinMessage } from '../index';
|
||||
import chunter from '../plugin'
|
||||
import { getTime } from '../utils'
|
||||
// import Share from './icons/Share.svelte'
|
||||
@@ -37,6 +38,7 @@
|
||||
export let message: WithLookup<Message>
|
||||
export let employees: Map<Ref<Employee>, Employee>
|
||||
export let thread: boolean = false
|
||||
export let isPinned: boolean = false
|
||||
|
||||
$: employee = getEmployee(message)
|
||||
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
|
||||
@@ -59,24 +61,39 @@
|
||||
action: chunter.actionImpl.SubscribeMessage
|
||||
} as Action)
|
||||
|
||||
async function deleteMessage () {
|
||||
await client.remove(message)
|
||||
const loc = getCurrentLocation()
|
||||
$: pinActions = isPinned
|
||||
? ({
|
||||
label: chunter.string.UnpinMessage,
|
||||
action: chunter.actionImpl.UnpinMessage
|
||||
} as Action)
|
||||
: ({
|
||||
label: chunter.string.PinMessage,
|
||||
action: chunter.actionImpl.PinMessage
|
||||
} as Action)
|
||||
|
||||
if (loc.path[3] === message._id) {
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
$: isEditing = false;
|
||||
|
||||
const editAction = {
|
||||
label: chunter.string.EditMessage,
|
||||
action: () => isEditing = true
|
||||
}
|
||||
|
||||
const deleteAction = {
|
||||
label: chunter.string.DeleteMessage,
|
||||
action: deleteMessage
|
||||
action: async () => {
|
||||
(await client.findAll(chunter.class.ThreadMessage, {attachedTo: message._id})).forEach(c => {
|
||||
UnpinMessage(c)
|
||||
})
|
||||
UnpinMessage(message)
|
||||
await client.remove(message)
|
||||
}
|
||||
}
|
||||
|
||||
const showMenu = async (ev: Event): Promise<void> => {
|
||||
const actions = await getActions(client, message, chunter.class.Message)
|
||||
actions.push(subscribeAction)
|
||||
actions.push(pinActions)
|
||||
|
||||
showPopup(
|
||||
Menu,
|
||||
{
|
||||
@@ -89,13 +106,28 @@
|
||||
await impl(message)
|
||||
}
|
||||
})),
|
||||
...(getCurrentAccount()._id === message.createBy ? [deleteAction] : [])
|
||||
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
|
||||
]
|
||||
},
|
||||
ev.target as HTMLElement
|
||||
)
|
||||
}
|
||||
|
||||
async function onMessageEdit (event: CustomEvent) {
|
||||
const { message: newContent, attachments: newAttachments } = event.detail
|
||||
|
||||
if (newContent !== message.content || newAttachments !== attachments) {
|
||||
await client.update(
|
||||
message,
|
||||
{
|
||||
content: newContent,
|
||||
attachments: newAttachments
|
||||
}
|
||||
)
|
||||
}
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
function getEmployee (message: WithLookup<Message>): Employee | undefined {
|
||||
const employee = (message.$lookup?.createBy as EmployeeAccount).employee
|
||||
if (employee !== undefined) {
|
||||
@@ -115,8 +147,18 @@
|
||||
{#if employee}{formatName(employee.name)}{/if}
|
||||
<span>{getTime(message.createOn)}</span>
|
||||
</div>
|
||||
<div class="text"><MessageViewer message={message.content} /></div>
|
||||
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
|
||||
{#if isEditing}
|
||||
<AttachmentRefInput
|
||||
space={message.space}
|
||||
_class={chunter.class.Comment}
|
||||
objectId={message._id}
|
||||
content={message.content}
|
||||
on:message={onMessageEdit}
|
||||
/>
|
||||
{:else}
|
||||
<div class="text"><MessageViewer message={message.content} /></div>
|
||||
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
|
||||
{/if}
|
||||
{#if reactions || message.replies}
|
||||
<div class="footer flex-col">
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { ChunterMessage } from '@anticrm/chunter'
|
||||
import { Ref, Space } from '@anticrm/core'
|
||||
import { Label, showPopup } from '@anticrm/ui'
|
||||
import PinnedMessagesPopup from './PinnedMessagesPopup.svelte'
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let space: Ref<Space>
|
||||
export let pinnedIds: Ref<ChunterMessage>[]
|
||||
|
||||
function showMessages (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
|
||||
showPopup(PinnedMessagesPopup, { space }, ev.target)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if pinnedIds.length > 0}
|
||||
<div class="bottom-divider over-underline pt-2 pb-2 container">
|
||||
<div on:click={(ev) => showMessages(ev)}>
|
||||
<Label label={chunter.string.Pinned} />
|
||||
{pinnedIds.length}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import chunter, { ChunterMessage } from '@anticrm/chunter'
|
||||
import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import { Ref, Space } from '@anticrm/core'
|
||||
import { Avatar, createQuery, getClient, MessageViewer } from '@anticrm/presentation'
|
||||
import { IconClose } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { UnpinMessage } from '../index'
|
||||
import { getTime } from '../utils'
|
||||
|
||||
export let space: Ref<Space>
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
let pinnedIds: Ref<ChunterMessage>[] = []
|
||||
pinnedQuery.query(
|
||||
chunter.class.Channel,
|
||||
{ _id: space },
|
||||
(res) => {
|
||||
pinnedIds = res[0]?.pinned ?? []
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
const messagesQuery = createQuery()
|
||||
let pinnedMessages: ChunterMessage[] = []
|
||||
|
||||
$: pinnedIds &&
|
||||
messagesQuery.query(chunter.class.ChunterMessage, { _id: { $in: pinnedIds } }, (res) => {
|
||||
pinnedMessages = res
|
||||
})
|
||||
|
||||
const employeeAccoutsQuery = createQuery()
|
||||
let employeeAcounts: EmployeeAccount[]
|
||||
|
||||
employeeAccoutsQuery.query(contact.class.EmployeeAccount, {}, (res) => (employeeAcounts = res))
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function getAvatar (_id?: Ref<Employee>): Promise<string | undefined | null> {
|
||||
if (_id === undefined) return (await client.findOne(contact.class.Employee, { _id }))?.avatar
|
||||
}
|
||||
|
||||
function getEmployee (message: ChunterMessage): EmployeeAccount | undefined {
|
||||
return employeeAcounts?.find((e) => e._id === message.createBy)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup vScroll popup">
|
||||
{#each pinnedMessages as message}
|
||||
<div class="message">
|
||||
<div class="header">
|
||||
{#await getEmployee(message) then employeeAccount}
|
||||
{#await getAvatar(employeeAccount?.employee) then avatar}
|
||||
<div class="avatar">
|
||||
<Avatar size={'medium'} {avatar} />
|
||||
</div>
|
||||
{/await}
|
||||
<span class="name">
|
||||
{formatName(employeeAccount?.name ?? '')}
|
||||
</span>
|
||||
{/await}
|
||||
<div
|
||||
class="cross"
|
||||
on:click={async () => {
|
||||
if (pinnedIds.length === 1)
|
||||
dispatch('close')
|
||||
UnpinMessage(message)
|
||||
}}
|
||||
>
|
||||
<IconClose size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<MessageViewer message={message.content} />
|
||||
<span class="time">{getTime(message.createOn)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
padding: 1.25rem 1.25rem 1.25rem;
|
||||
max-height: 20rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.75rem 1rem 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: inherit;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
margin-left: 1rem;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.cross {
|
||||
opacity: 0.4;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Attachment } from '@anticrm/attachment'
|
||||
import { AttachmentList } from '@anticrm/attachment-resources'
|
||||
import { AttachmentList, AttachmentRefInput } from '@anticrm/attachment-resources'
|
||||
import type { ThreadMessage } from '@anticrm/chunter'
|
||||
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
|
||||
@@ -24,6 +24,7 @@
|
||||
import { ActionIcon, IconMoreH, Menu, showPopup } from '@anticrm/ui'
|
||||
import { Action } from '@anticrm/view'
|
||||
import { getActions } from '@anticrm/view-resources'
|
||||
import { UnpinMessage } from '../index';
|
||||
import chunter from '../plugin'
|
||||
import { getTime } from '../utils'
|
||||
// import Share from './icons/Share.svelte'
|
||||
@@ -33,6 +34,7 @@
|
||||
|
||||
export let message: WithLookup<ThreadMessage>
|
||||
export let employees: Map<Ref<Employee>, Employee>
|
||||
export let isPinned: boolean = false
|
||||
|
||||
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
|
||||
|
||||
@@ -53,14 +55,35 @@
|
||||
action: chunter.actionImpl.SubscribeComment
|
||||
} as Action)
|
||||
|
||||
$: pinActions = isPinned
|
||||
? ({
|
||||
label: chunter.string.UnpinMessage,
|
||||
action: chunter.actionImpl.UnpinMessage
|
||||
} as Action)
|
||||
: ({
|
||||
label: chunter.string.PinMessage,
|
||||
action: chunter.actionImpl.PinMessage
|
||||
} as Action)
|
||||
|
||||
$: isEditing = false;
|
||||
|
||||
const editAction = {
|
||||
label: chunter.string.EditMessage,
|
||||
action: () => isEditing = true
|
||||
}
|
||||
|
||||
const deleteAction = {
|
||||
label: chunter.string.DeleteMessage,
|
||||
action: async () => await client.removeDoc(message._class, message.space, message._id)
|
||||
action: async () => {
|
||||
await client.removeDoc(message._class, message.space, message._id)
|
||||
UnpinMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
const showMenu = async (ev: Event): Promise<void> => {
|
||||
const actions = await getActions(client, message, chunter.class.ThreadMessage)
|
||||
actions.push(subscribeAction)
|
||||
actions.push(pinActions)
|
||||
showPopup(
|
||||
Menu,
|
||||
{
|
||||
@@ -73,13 +96,28 @@
|
||||
await impl(message)
|
||||
}
|
||||
})),
|
||||
...(getCurrentAccount()._id === message.createBy ? [deleteAction] : [])
|
||||
...(getCurrentAccount()._id === message.createBy ? [editAction, deleteAction] : [])
|
||||
]
|
||||
},
|
||||
ev.target as HTMLElement
|
||||
)
|
||||
}
|
||||
|
||||
async function onMessageEdit (event: CustomEvent) {
|
||||
const { message: newContent, attachments: newAttachments } = event.detail
|
||||
|
||||
if (newContent !== message.content || newAttachments !== attachments) {
|
||||
await client.update(
|
||||
message,
|
||||
{
|
||||
content: newContent,
|
||||
attachments: newAttachments
|
||||
}
|
||||
)
|
||||
}
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
$: employee = getEmployee(message)
|
||||
|
||||
function getEmployee (comment: WithLookup<ThreadMessage>): Employee | undefined {
|
||||
@@ -97,8 +135,18 @@
|
||||
{#if employee}{formatName(employee.name)}{/if}
|
||||
<span>{getTime(message.createOn)}</span>
|
||||
</div>
|
||||
<div class="text"><MessageViewer message={message.content} /></div>
|
||||
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
|
||||
{#if isEditing}
|
||||
<AttachmentRefInput
|
||||
space={message.space}
|
||||
_class={chunter.class.Comment}
|
||||
objectId={message._id}
|
||||
content={message.content}
|
||||
on:message={onMessageEdit}
|
||||
/>
|
||||
{:else}
|
||||
<div class="text"><MessageViewer message={message.content} /></div>
|
||||
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
|
||||
{/if}
|
||||
{#if reactions}
|
||||
<div class="footer">
|
||||
<div><Reactions /></div>
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import { AttachmentRefInput } from '@anticrm/attachment-resources'
|
||||
import type { ThreadMessage, Message } from '@anticrm/chunter'
|
||||
import type { ThreadMessage, Message, ChunterMessage, Channel } from '@anticrm/chunter'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import core, { Doc, generateId, getCurrentAccount, Ref, Space, TxFactory } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { IconClose, Label } from '@anticrm/ui'
|
||||
import { IconClose, Label, getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import { afterUpdate, beforeUpdate, createEventDispatcher } from 'svelte'
|
||||
import { createBacklinks } from '../backlinks'
|
||||
import chunter from '../plugin'
|
||||
@@ -57,6 +57,9 @@
|
||||
createBy: core.class.Account
|
||||
}
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
let pinnedIds: Ref<ChunterMessage>[] = []
|
||||
|
||||
$: updateQueries(_id)
|
||||
|
||||
function updateQueries (id: Ref<Message>) {
|
||||
@@ -65,7 +68,15 @@
|
||||
{
|
||||
_id: id
|
||||
},
|
||||
(res) => (message = res[0]),
|
||||
(res) => {
|
||||
message = res[0]
|
||||
|
||||
if (!message) {
|
||||
const loc = getCurrentLocation()
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
_id: { attachments: attachment.class.Attachment },
|
||||
@@ -88,6 +99,15 @@
|
||||
lookup
|
||||
}
|
||||
)
|
||||
|
||||
pinnedQuery.query(
|
||||
chunter.class.Channel,
|
||||
{ _id: currentSpace },
|
||||
(res) => {
|
||||
pinnedIds = res[0]?.pinned ?? []
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
|
||||
let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
|
||||
@@ -174,7 +194,11 @@
|
||||
{#if newMessagesPos === i}
|
||||
<ChannelSeparator title={chunter.string.New} line reverse isNew />
|
||||
{/if}
|
||||
<ThreadComment message={comment} {employees} />
|
||||
<ThreadComment
|
||||
message={comment}
|
||||
{employees}
|
||||
isPinned={pinnedIds.includes(comment._id)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import chunter, { Message, ThreadMessage } from '@anticrm/chunter'
|
||||
import core from '@anticrm/core'
|
||||
import chunter, { Channel, ChunterMessage, Message, ThreadMessage } from '@anticrm/chunter'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { Resources } from '@anticrm/platform'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
|
||||
import TxBacklinkReference from './components/activity/TxBacklinkReference.svelte'
|
||||
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
|
||||
@@ -62,6 +64,22 @@ async function UnsubscribeComment (object: ThreadMessage): Promise<void> {
|
||||
await client.unsubscribe(object.attachedTo)
|
||||
}
|
||||
|
||||
async function PinMessage (message: ChunterMessage): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
await client.updateDoc<Channel>(chunter.class.Channel, core.space.Space, message.space, {
|
||||
$push: { pinned: message._id }
|
||||
})
|
||||
}
|
||||
|
||||
export async function UnpinMessage (message: ChunterMessage): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
await client.updateDoc<Channel>(chunter.class.Channel, core.space.Space, message.space, {
|
||||
$pull: { pinned: message._id }
|
||||
})
|
||||
}
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
CommentInput,
|
||||
@@ -85,6 +103,8 @@ export default async (): Promise<Resources> => ({
|
||||
SubscribeMessage,
|
||||
SubscribeComment,
|
||||
UnsubscribeMessage,
|
||||
UnsubscribeComment
|
||||
UnsubscribeComment,
|
||||
PinMessage,
|
||||
UnpinMessage
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,7 +30,9 @@ export default mergeIds(chunterId, chunter, {
|
||||
SubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
|
||||
SubscribeComment: '' as Resource<(object: Doc) => Promise<void>>,
|
||||
UnsubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
|
||||
UnsubscribeComment: '' as Resource<(object: Doc) => Promise<void>>
|
||||
UnsubscribeComment: '' as Resource<(object: Doc) => Promise<void>>,
|
||||
PinMessage: '' as Resource<(object: Doc) => Promise<void>>,
|
||||
UnpinMessage: '' as Resource<(object: Doc) => Promise<void>>
|
||||
},
|
||||
string: {
|
||||
Channel: '' as IntlString,
|
||||
@@ -51,6 +53,10 @@ export default mergeIds(chunterId, chunter, {
|
||||
New: '' as IntlString,
|
||||
GetNewReplies: '' as IntlString,
|
||||
TurnOffReplies: '' as IntlString,
|
||||
DeleteMessage: '' as IntlString
|
||||
PinMessage: '' as IntlString,
|
||||
UnpinMessage: '' as IntlString,
|
||||
Pinned: '' as IntlString,
|
||||
DeleteMessage: '' as IntlString,
|
||||
EditMessage: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AnyComponent } from '@anticrm/ui'
|
||||
*/
|
||||
export interface Channel extends Space {
|
||||
lastMessage?: Timestamp
|
||||
pinned?: Ref<ChunterMessage>[]
|
||||
topic?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Dropdown } from '@anticrm/ui'
|
||||
import type { TooltipAligment } from '@anticrm/ui'
|
||||
import type { TooltipAlignment, ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import { ListItem } from '@anticrm/ui/src/types'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
@@ -28,11 +28,11 @@
|
||||
export let value: Ref<Organization> | undefined
|
||||
export let label: IntlString = contact.string.Organization
|
||||
|
||||
export let kind: 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'dangerous' = 'no-border'
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'small'
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let labelDirection: TooltipAligment | undefined = undefined
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
|
||||
const query = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="star" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M4.8,14.5c-0.3,0-0.5-0.1-0.7-0.2c-0.4-0.3-0.6-0.8-0.6-1.3L4,10.1c0-0.1,0-0.2-0.1-0.3l-2-2C1.5,7.4,1.4,6.8,1.6,6.3 c0.2-0.5,0.6-0.8,1.1-0.9L5.4,5c0.1,0,0.2-0.1,0.2-0.2l1.2-2.6C7,1.8,7.5,1.5,8,1.5s1,0.3,1.2,0.8l0,0l1.2,2.6 c0,0.1,0.1,0.2,0.2,0.2l2.7,0.4c0.5,0.1,0.9,0.4,1.1,0.9c0.2,0.5,0,1-0.3,1.4l-2,2C12.1,9.8,12,10,12,10.1l0.5,2.8 c0.1,0.5-0.1,1-0.6,1.3c-0.4,0.3-0.9,0.3-1.4,0.1L8.1,13c-0.1,0-0.2,0-0.3,0l-2.4,1.3C5.2,14.4,5,14.5,4.8,14.5z M8,2.5 c-0.1,0-0.2,0-0.3,0.2L6.5,5.3C6.3,5.7,5.9,6,5.5,6L2.8,6.4c-0.2,0-0.2,0.2-0.3,0.2c0,0.1-0.1,0.2,0.1,0.4l2,2 C4.9,9.4,5,9.8,4.9,10.2l-0.5,2.8c0,0.2,0.1,0.3,0.1,0.4c0.1,0,0.2,0.1,0.3,0l2.4-1.3c0.4-0.2,0.9-0.2,1.2,0l2.4,1.3 c0.1,0.1,0.3,0,0.3,0c0,0,0.2-0.1,0.1-0.4l-0.5-2.8c-0.1-0.4,0.1-0.9,0.4-1.2l2-2c0.1-0.2,0.1-0.3,0.1-0.4c0-0.1-0.1-0.2-0.3-0.2 L10.5,6c-0.4-0.1-0.8-0.3-1-0.7L8.3,2.7C8.2,2.5,8.1,2.5,8,2.5z"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1006 B |
@@ -0,0 +1,18 @@
|
||||
// The "rig.json" file directs tools to look for their config files in an external package.
|
||||
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
|
||||
/**
|
||||
* (Required) The name of the rig package to inherit from.
|
||||
* It should be an NPM package name with the "-rig" suffix.
|
||||
*/
|
||||
"rigPackageName": "@anticrm/platform-rig"
|
||||
|
||||
/**
|
||||
* (Optional) Selects a config profile from the rig package. The name must consist of
|
||||
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
|
||||
* If omitted, then the "default" profile will be used."
|
||||
*/
|
||||
// "rigProfile": "your-profile-name"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"string": {
|
||||
"DeleteStarred": "Delete starred",
|
||||
"Starred": "Starred",
|
||||
"Star": "Star",
|
||||
"Unstar": "Unstar"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"string": {
|
||||
"DeleteStarred": "Удалить закладки",
|
||||
"Starred": "Закладки",
|
||||
"Star": "Добавить в закладки",
|
||||
"Unstar": "Удалить из закладок"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@anticrm/preference-assets",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "heft build",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"format": "prettier --write src && eslint --fix src",
|
||||
"build:watch": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anticrm/platform-rig": "~0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"eslint": "^7.32.0",
|
||||
"prettier": "^2.4.1",
|
||||
"@types/heft-jest": "^1.0.2",
|
||||
"@rushstack/heft": "^0.41.1",
|
||||
"typescript": "^4.3.5",
|
||||
"@types/node": "~16.11.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anticrm/platform": "~0.6.5",
|
||||
"@anticrm/preference": "~0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Copyright © 2022 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.
|
||||
//
|
||||
|
||||
import { addStringsLoader, loadMetadata } from '@anticrm/platform'
|
||||
import preference, { preferenceId } from '@anticrm/preference'
|
||||
|
||||
const icons = require('../assets/icons.svg') as string // eslint-disable-line
|
||||
loadMetadata(preference.icon, {
|
||||
Star: `${icons}#star`
|
||||
})
|
||||
|
||||
addStringsLoader(preferenceId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,18 @@
|
||||
// The "rig.json" file directs tools to look for their config files in an external package.
|
||||
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
|
||||
/**
|
||||
* (Required) The name of the rig package to inherit from.
|
||||
* It should be an NPM package name with the "-rig" suffix.
|
||||
*/
|
||||
"rigPackageName": "@anticrm/platform-rig"
|
||||
|
||||
/**
|
||||
* (Optional) Selects a config profile from the rig package. The name must consist of
|
||||
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
|
||||
* If omitted, then the "default" profile will be used."
|
||||
*/
|
||||
// "rigProfile": "your-profile-name"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@anticrm/preference",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "heft build",
|
||||
"build:watch": "tsc",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"lint": "eslint src",
|
||||
"format": "prettier --write src && eslint --fix src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anticrm/platform-rig": "~0.6.0",
|
||||
"@types/heft-jest": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint": "^7.32.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
||||
"prettier": "^2.4.1",
|
||||
"@rushstack/heft": "^0.41.1",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anticrm/platform": "~0.6.5",
|
||||
"@anticrm/core": "~0.6.16",
|
||||
"@anticrm/ui": "~0.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// Copyright © 2022 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.
|
||||
//
|
||||
|
||||
import type { Class, Doc, Domain, Ref, Space } from '@anticrm/core'
|
||||
import type { Asset, IntlString, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Preference extends Doc {
|
||||
attachedTo: Ref<Doc>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SpacePreference extends Preference {
|
||||
attachedTo: Ref<Space>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const preferenceId = 'preference' as Plugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DOMAIN_PREFERENCE = 'preference' as Domain
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
const preference = plugin(preferenceId, {
|
||||
class: {
|
||||
Preference: '' as Ref<Class<Preference>>,
|
||||
SpacePreference: '' as Ref<Class<SpacePreference>>
|
||||
},
|
||||
space: {
|
||||
Preference: '' as Ref<Space>
|
||||
},
|
||||
icon: {
|
||||
Star: '' as Asset
|
||||
},
|
||||
string: {
|
||||
DeleteStarred: '' as IntlString,
|
||||
Starred: '' as IntlString,
|
||||
Star: '' as IntlString,
|
||||
Unstar: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
export default preference
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"lib": ["esnext", "dom"]
|
||||
}
|
||||
}
|
||||
@@ -165,18 +165,6 @@
|
||||
placeholder={recruit.string.Location} bind:value={location}
|
||||
maxWidth={'37.5rem'} kind={'small-style'}
|
||||
/>
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={doc.participants}
|
||||
title={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
doc.participants = [...(doc.participants ?? []), evt.detail._id]
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
doc.participants = doc.participants?.filter((it) => it !== evt.detail._id) ?? [currentUser.employee]
|
||||
}}
|
||||
noItems={calendar.string.NoParticipants}
|
||||
/>
|
||||
<StyledTextBox
|
||||
emphasized
|
||||
showButtons={false}
|
||||
@@ -199,5 +187,14 @@
|
||||
/>
|
||||
<DateRangePresenter bind:value={startDate} labelNull={recruit.string.StartDate} withTime editable on:change={updateStart} />
|
||||
<DateRangePresenter bind:value={dueDate} labelNull={recruit.string.DueDate} withTime editable />
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={doc.participants}
|
||||
label={calendar.string.Participants}
|
||||
on:update={(evt) => {
|
||||
doc.participants = evt.detail
|
||||
}}
|
||||
noItems={calendar.string.NoParticipants}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={object.participants}
|
||||
title={calendar.string.Participants}
|
||||
label={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
client.update(object, { $push: { participants: evt.detail._id } })
|
||||
}}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"Canceled": "Canceled",
|
||||
"SetPriority": "Set priority\u2026",
|
||||
"SetStatus": "Set status\u2026",
|
||||
"SelectIssue": "Select issue",
|
||||
"Priority": "Priority",
|
||||
"NoPriority": "No priority",
|
||||
"Urgent": "Urgent",
|
||||
@@ -51,7 +52,7 @@
|
||||
"Project": "Project",
|
||||
"Space": "",
|
||||
"DueDate": "Set due date\u2026",
|
||||
"ModificationDate": "Last Modified: {value}",
|
||||
"ModificationDate": "Updated {value}",
|
||||
"Team": "",
|
||||
"Issue": "",
|
||||
"Document": "",
|
||||
@@ -60,7 +61,11 @@
|
||||
"Rank": "",
|
||||
"IssueTitlePlaceholder": "Issue title",
|
||||
"IssueDescriptionPlaceholder": "Add description",
|
||||
"AddIssueTooltip": "Add issue..."
|
||||
"AddIssueTooltip": "Add issue...",
|
||||
"DueDatePopupTitle": "Due on {value}",
|
||||
"DueDatePopupOverdueTitle": "Was due on {value}",
|
||||
"DueDatePopupDescription": "{value, plural, =0 {Today} =1 {Tomorrow} other {# days remaining}}",
|
||||
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 day overdue} other {# days overdue}}"
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -91,8 +91,6 @@
|
||||
{ icon: tracker.icon.Parent, label: tracker.string.Parent }
|
||||
]
|
||||
|
||||
let issueDate: number | null = null
|
||||
|
||||
const handlePriorityChanged = (newPriority: IssuePriority | undefined) => {
|
||||
if (newPriority === undefined) {
|
||||
return
|
||||
@@ -166,7 +164,7 @@
|
||||
size="small"
|
||||
kind="no-border"
|
||||
/>
|
||||
<DatePresenter value={issueDate} editable />
|
||||
<DatePresenter bind:value={object.dueDate} editable />
|
||||
<Button
|
||||
icon={tracker.icon.MoreActions}
|
||||
width="min-content"
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class="flex-presenter" on:click={handlePriorityEditorOpened}>
|
||||
<div class="icon">
|
||||
<div class="priorityIcon">
|
||||
<Icon icon={issuePriorities[priority].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
@@ -62,3 +62,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.priorityIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class="flex-presenter" on:click={handleStatusEditorOpened}>
|
||||
<div class="icon">
|
||||
<div class="statusIcon">
|
||||
<Icon icon={issueStatuses[status].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
@@ -62,3 +62,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.statusIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,11 +49,14 @@
|
||||
<Scroller>
|
||||
<IssuesList
|
||||
_class={tracker.class.Issue}
|
||||
config={[
|
||||
leftItemsConfig={[
|
||||
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
|
||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
|
||||
{ key: '', presenter: tracker.component.StatusPresenter, props: { currentSpace } },
|
||||
{ key: '', presenter: tracker.component.TitlePresenter },
|
||||
{ key: '', presenter: tracker.component.TitlePresenter }
|
||||
]}
|
||||
rightItemsConfig={[
|
||||
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
{ key: '', presenter: tracker.component.AssigneePresenter, props: { currentSpace } }
|
||||
]}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { Icon, Label, IconDPCalendarOver, IconDPCalendar } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let formattedDate: string = ''
|
||||
export let daysDifference: number = 0
|
||||
export let isOverdue: boolean = false
|
||||
export let iconModifier: 'warning' | 'critical' | 'overdue' | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if formattedDate}
|
||||
<div class="root">
|
||||
<div
|
||||
class="iconContainer"
|
||||
class:mIconContainerWarning={iconModifier === 'warning'}
|
||||
class:mIconContainerCritical={iconModifier === 'critical' || iconModifier === 'overdue'}
|
||||
>
|
||||
<Icon icon={isOverdue ? IconDPCalendarOver : IconDPCalendar} size={'small'} />
|
||||
</div>
|
||||
<div class="messageContainer">
|
||||
<div class="title">
|
||||
<Label
|
||||
label={isOverdue ? tracker.string.DueDatePopupOverdueTitle : tracker.string.DueDatePopupTitle}
|
||||
params={{ value: formattedDate }}
|
||||
/>
|
||||
</div>
|
||||
<div class="description">
|
||||
<Label
|
||||
label={isOverdue ? tracker.string.DueDatePopupOverdueDescription : tracker.string.DueDatePopupDescription}
|
||||
params={{ value: daysDifference }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
color: var(--content-color);
|
||||
margin-right: 1rem;
|
||||
|
||||
&.mIconContainerWarning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.mIconContainerCritical {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--theme-caption-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { Ref, Timestamp } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { DatePresenter, Tooltip, getDaysDifference } from '@anticrm/ui'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import DueDatePopup from './DueDatePopup.svelte'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: Issue
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
|
||||
const WARNING_DAYS = 7
|
||||
const client = getClient()
|
||||
|
||||
$: today = new Date(new Date(Date.now()).setHours(0, 0, 0, 0))
|
||||
$: dueDateMs = value.dueDate
|
||||
$: isOverdue = dueDateMs !== null && dueDateMs < today.getTime()
|
||||
$: dueDate = dueDateMs === null ? null : new Date(dueDateMs)
|
||||
$: daysDifference = dueDate === null ? null : getDaysDifference(today, dueDate)
|
||||
$: iconModifier = getIconModifier(isOverdue, daysDifference)
|
||||
$: formattedDate = !dueDateMs ? '' : new Date(dueDateMs).toLocaleString('default', { month: 'short', day: 'numeric' })
|
||||
|
||||
const handleDueDateChanged = async (event: CustomEvent<Timestamp>) => {
|
||||
const newDate = event.detail
|
||||
|
||||
if (newDate === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(currentIssue, { dueDate: newDate })
|
||||
}
|
||||
|
||||
const getIconModifier = (isOverdue: boolean, daysDifference: number | null) => {
|
||||
if (isOverdue) {
|
||||
return 'overdue' as 'overdue' // Fixes `DatePresenter` icon type issue
|
||||
}
|
||||
|
||||
if (daysDifference === 0) {
|
||||
return 'critical' as 'critical'
|
||||
}
|
||||
|
||||
if (daysDifference !== null && daysDifference <= WARNING_DAYS) {
|
||||
return 'warning' as 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
$: shouldRenderPresenter = dueDateMs && value.status !== IssueStatus.Done && value.status !== IssueStatus.Canceled
|
||||
</script>
|
||||
|
||||
{#if shouldRenderPresenter}
|
||||
<Tooltip
|
||||
direction={'top'}
|
||||
component={DueDatePopup}
|
||||
props={{
|
||||
formattedDate: formattedDate,
|
||||
daysDifference: daysDifference,
|
||||
isOverdue: isOverdue,
|
||||
iconModifier: iconModifier
|
||||
}}
|
||||
>
|
||||
<DatePresenter
|
||||
value={dueDateMs}
|
||||
editable={true}
|
||||
shouldShowLabel={false}
|
||||
icon={iconModifier}
|
||||
on:change={handleDueDateChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
@@ -16,14 +16,16 @@
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, getObjectValue } from '@anticrm/core'
|
||||
import { SortingOrder } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { CheckBox, Loading, showPopup, Spinner, IconMoreV } from '@anticrm/ui'
|
||||
import { CheckBox, Loading, showPopup, Spinner, IconMoreV, Tooltip } from '@anticrm/ui'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { buildModel, LoadingProps, Menu } from '@anticrm/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let config: (BuildModelKey | string)[]
|
||||
export let leftItemsConfig: (BuildModelKey | string)[]
|
||||
export let rightItemsConfig: (BuildModelKey | string)[] | undefined = undefined
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let query: DocumentQuery<Doc>
|
||||
|
||||
@@ -92,13 +94,20 @@
|
||||
|
||||
return props.length
|
||||
}
|
||||
|
||||
const buildItemModels = async () => {
|
||||
const leftModels = await buildModel({ client, _class, keys: leftItemsConfig, options })
|
||||
const rightModels = rightItemsConfig && (await buildModel({ client, _class, keys: rightItemsConfig, options }))
|
||||
|
||||
return { leftModels, rightModels }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: config, options })}
|
||||
{#await buildItemModels()}
|
||||
{#if !isLoading}
|
||||
<Loading />
|
||||
{/if}
|
||||
{:then attributeModels}
|
||||
{:then itemModels}
|
||||
<div class="listRoot">
|
||||
{#if docObjects}
|
||||
{#each docObjects as docObject, rowIndex (docObject._id)}
|
||||
@@ -107,65 +116,80 @@
|
||||
class:mListGridChecked={selectedIssueIds.has(docObject._id)}
|
||||
class:mListGridFixed={rowIndex === selectedRowIndex}
|
||||
>
|
||||
{#each attributeModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<div class="eListGridCheckBox ml-2">
|
||||
<CheckBox
|
||||
checked={selectedIssueIds.has(docObject._id)}
|
||||
on:value={(event) => {
|
||||
handleIssueSelected(docObject._id, event)
|
||||
}}
|
||||
/>
|
||||
<div class="modelsContainer">
|
||||
{#each itemModels.leftModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SelectIssue}>
|
||||
<div class="eListGridCheckBox ml-2">
|
||||
<CheckBox
|
||||
checked={selectedIssueIds.has(docObject._id)}
|
||||
on:value={(event) => {
|
||||
handleIssueSelected(docObject._id, event)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="priorityPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="priorityPresenter">
|
||||
{:else if attributeModelIndex === 1}
|
||||
<div class="issuePresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
<div
|
||||
id="context-menu"
|
||||
class="eIssuePresenterContextMenu"
|
||||
on:click={(event) => showMenu(event, docObject, rowIndex)}
|
||||
>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 1}
|
||||
<div class="issuePresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
<div
|
||||
id="context-menu"
|
||||
class="eIssuePresenterContextMenu"
|
||||
on:click={(event) => showMenu(event, docObject, rowIndex)}
|
||||
>
|
||||
<IconMoreV size={'small'} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if itemModels.rightModels}
|
||||
<div class="modelsContainer">
|
||||
{#each itemModels.rightModels as attributeModel}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="gridElement">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if loadingProps !== undefined}
|
||||
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
|
||||
<div class="listGrid mListGridIsLoading" class:fixed={rowIndex === selectedRowIndex}>
|
||||
{#each attributeModels as _, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
<div class="modelsContainer">
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -182,8 +206,9 @@
|
||||
}
|
||||
|
||||
.listGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 5rem 2rem auto 4rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 3.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
@@ -203,7 +228,7 @@
|
||||
}
|
||||
|
||||
&.mListGridIsLoading {
|
||||
grid-template-columns: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -225,24 +250,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.checkBox {
|
||||
.modelsContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.03rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(247, 248, 248, 0.5);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.priorityPresenter {
|
||||
@@ -252,7 +273,7 @@
|
||||
.issuePresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
|
||||
.eIssuePresenterContextMenu {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Issue, IssuePriority, Team } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import PrioritySelector from '../PrioritySelector.svelte'
|
||||
|
||||
@@ -40,10 +41,12 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<PrioritySelector
|
||||
kind={'icon'}
|
||||
shouldShowLabel={false}
|
||||
priority={value.priority}
|
||||
onPriorityChange={handlePriorityChanged}
|
||||
/>
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SetPriority}>
|
||||
<PrioritySelector
|
||||
kind={'icon'}
|
||||
shouldShowLabel={false}
|
||||
priority={value.priority}
|
||||
onPriorityChange={handlePriorityChanged}
|
||||
/>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import StatusSelector from '../StatusSelector.svelte'
|
||||
|
||||
@@ -40,5 +41,7 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<StatusSelector kind={'icon'} shouldShowLabel={false} status={value.status} onStatusChange={handleStatusChanged} />
|
||||
<Tooltip direction={'bottom'} label={tracker.string.SetStatus}>
|
||||
<StatusSelector kind={'icon'} shouldShowLabel={false} status={value.status} onStatusChange={handleStatusChanged} />
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
@@ -29,6 +29,7 @@ import IssuePresenter from './components/issues/IssuePresenter.svelte'
|
||||
import TitlePresenter from './components/issues/TitlePresenter.svelte'
|
||||
import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
|
||||
import StatusPresenter from './components/issues/StatusPresenter.svelte'
|
||||
import DueDatePresenter from './components/issues/DueDatePresenter.svelte'
|
||||
import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
|
||||
|
||||
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
|
||||
@@ -52,6 +53,7 @@ export default async (): Promise<Resources> => ({
|
||||
PriorityPresenter,
|
||||
StatusPresenter,
|
||||
AssigneePresenter,
|
||||
DueDatePresenter,
|
||||
EditIssue,
|
||||
NewIssueHeader
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
AddIssue: '' as IntlString,
|
||||
NewIssue: '' as IntlString,
|
||||
Team: '' as IntlString,
|
||||
SelectIssue: '' as IntlString,
|
||||
SelectTeam: '' as IntlString,
|
||||
SaveIssue: '' as IntlString,
|
||||
Todo: '' as IntlString,
|
||||
@@ -76,6 +77,10 @@ export default mergeIds(trackerId, tracker, {
|
||||
DocumentIcon: '' as IntlString,
|
||||
DocumentColor: '' as IntlString,
|
||||
Rank: '' as IntlString,
|
||||
DueDatePopupTitle: '' as IntlString,
|
||||
DueDatePopupOverdueTitle: '' as IntlString,
|
||||
DueDatePopupDescription: '' as IntlString,
|
||||
DueDatePopupOverdueDescription: '' as IntlString,
|
||||
|
||||
IssueTitlePlaceholder: '' as IntlString,
|
||||
IssueDescriptionPlaceholder: '' as IntlString,
|
||||
@@ -98,6 +103,7 @@ export default mergeIds(trackerId, tracker, {
|
||||
PriorityPresenter: '' as AnyComponent,
|
||||
StatusPresenter: '' as AnyComponent,
|
||||
AssigneePresenter: '' as AnyComponent,
|
||||
DueDatePresenter: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
CreateTeam: '' as AnyComponent,
|
||||
NewIssueHeader: '' as AnyComponent
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"@anticrm/calendar": "~0.6.0",
|
||||
"@anticrm/notification": "~0.6.0",
|
||||
"@anticrm/notification-resources": "~0.6.0",
|
||||
"@anticrm/preference": "~0.6.0",
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/view-resources": "~0.6.0"
|
||||
}
|
||||
|
||||
@@ -69,11 +69,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiPopup">
|
||||
<div class="ap-space" />
|
||||
<div class="ap-scroll"><div class="ap-box">
|
||||
<div class="selectPopup">
|
||||
<div class="scroll"><div class="box">
|
||||
{#await getItems() then items}
|
||||
<div class="ap-menuItem flex-row-center" on:click={() => { editProfile(items) }}>
|
||||
<div class="menu-item high flex-row-center" on:click={() => { editProfile(items) }}>
|
||||
{#if employee}
|
||||
<Avatar avatar={employee.avatar} size={'medium'} />
|
||||
{/if}
|
||||
@@ -86,7 +85,7 @@
|
||||
</div>
|
||||
{#if items}
|
||||
{#each filterItems(items) as item }
|
||||
<button class="ap-menuItem" on:click={() => selectCategory(item)}>
|
||||
<button class="menu-item" on:click={() => selectCategory(item)}>
|
||||
<div class='mr-2'>
|
||||
<Icon icon={item.icon} size={'small'}/>
|
||||
</div>
|
||||
@@ -94,13 +93,13 @@
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<button class="ap-menuItem" on:click={selectWorkspace}>
|
||||
<button class="menu-item" on:click={selectWorkspace}>
|
||||
<div class='mr-2'>
|
||||
<Icon icon={setting.icon.SelectWorkspace} size={'small'}/>
|
||||
</div>
|
||||
<Label label={setting.string.SelectWorkspace} />
|
||||
</button>
|
||||
<button class="ap-menuItem" on:click={signOut}>
|
||||
<button class="menu-item" on:click={signOut}>
|
||||
<div class='mr-2'>
|
||||
<Icon icon={setting.icon.Signout} size={'small'}/>
|
||||
</div>
|
||||
@@ -108,5 +107,4 @@
|
||||
</button>
|
||||
{/await}
|
||||
</div></div>
|
||||
<div class="ap-space" />
|
||||
</div>
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// Copyright © 2022 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 core, { Ref, SortingOrder, Space } from '@anticrm/core'
|
||||
import core, { Doc, Ref, SortingOrder, Space } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { Scroller } from '@anticrm/ui'
|
||||
import type { NavigatorModel, SpecialNavModel } from '@anticrm/workbench'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import preferece, { SpacePreference } from '@anticrm/preference'
|
||||
import { getSpecialSpaceClass } from '../utils'
|
||||
import preference from '@anticrm/preference'
|
||||
import SpacesNav from './navigator/SpacesNav.svelte'
|
||||
import SpecialElement from './navigator/SpecialElement.svelte'
|
||||
import StarredNav from './navigator/StarredNav.svelte'
|
||||
import TreeSeparator from './navigator/TreeSeparator.svelte'
|
||||
|
||||
export let model: NavigatorModel | undefined
|
||||
export let currentSpace: Ref<Space> | undefined
|
||||
export let currentSpecial: string | undefined
|
||||
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const query = createQuery()
|
||||
|
||||
let spaces: Space[] = []
|
||||
let starred: Space[] = []
|
||||
let shownSpaces: Space[] = []
|
||||
|
||||
|
||||
$: if (model) {
|
||||
query.query(
|
||||
core.class.Space,
|
||||
@@ -45,31 +50,31 @@
|
||||
{ sort: { name: SortingOrder.Ascending } })
|
||||
}
|
||||
|
||||
let specTopCount: number
|
||||
let specBottomCount: number
|
||||
let spModelCount: number
|
||||
|
||||
let topSpecials: SpecialNavModel[] = []
|
||||
let bottomSpecials: SpecialNavModel[] = []
|
||||
|
||||
async function update (model: NavigatorModel, spaces: Space[]) {
|
||||
let preferences: Map<Ref<Doc>, SpacePreference> = new Map<Ref<Doc>, SpacePreference>()
|
||||
|
||||
const preferenceQuery = createQuery()
|
||||
|
||||
preferenceQuery.query(preferece.class.SpacePreference, {}, (res) => {
|
||||
preferences = new Map(res.map((r) => { return [r.attachedTo, r] }))
|
||||
})
|
||||
|
||||
async function update (model: NavigatorModel, spaces: Space[], preferences: Map<Ref<Doc>, SpacePreference>) {
|
||||
if (model.specials !== undefined) {
|
||||
topSpecials = await getSpecials(model.specials, 'top', spaces)
|
||||
specTopCount = topSpecials.length
|
||||
bottomSpecials = await getSpecials(model.specials, 'bottom', spaces)
|
||||
specBottomCount = bottomSpecials.length
|
||||
} else {
|
||||
topSpecials = []
|
||||
bottomSpecials = []
|
||||
specBottomCount = 0
|
||||
specTopCount = 0
|
||||
}
|
||||
if (model.spaces) spModelCount = model.spaces.length
|
||||
shownSpaces = spaces.filter(sp => !sp.archived)
|
||||
shownSpaces = spaces.filter((sp) => !sp.archived && !preferences.has(sp._id))
|
||||
starred = spaces.filter((sp) => preferences.has(sp._id))
|
||||
}
|
||||
|
||||
$: if (model) update(model, spaces)
|
||||
|
||||
$: if (model) update(model, spaces, preferences)
|
||||
|
||||
async function getSpecials (specials: SpecialNavModel[], state: 'top' | 'bottom', spaces: Space[]): Promise<SpecialNavModel[]> {
|
||||
const result: SpecialNavModel[] = []
|
||||
for (const sp of specials) {
|
||||
@@ -105,6 +110,10 @@
|
||||
|
||||
{#if topSpecials.length > 0 || bottomSpecials.length > 0}<TreeSeparator />{/if}
|
||||
|
||||
{#if starred.length}
|
||||
<StarredNav label={preference.string.Starred} spaces={starred} on:space {currentSpace} />
|
||||
{/if}
|
||||
|
||||
{#each model.spaces as m (m.label)}
|
||||
<SpacesNav spaces={shownSpaces.filter(it => hierarchy.isDerived(it._class, m.spaceClass))} {currentSpace} model={m} on:space {currentSpecial}/>
|
||||
{/each}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Action, AnyComponent, IconAdd, IconEdit, showPanel, showPopup } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import preference from '@anticrm/preference'
|
||||
import { getActions as getContributedActions } from '@anticrm/view-resources'
|
||||
import { SpacesNavModel } from '@anticrm/workbench'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@@ -54,7 +55,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getEditor(_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
|
||||
const starSpace: Action = {
|
||||
label: preference.string.Star,
|
||||
icon: preference.icon.Star,
|
||||
action: async (_id: Ref<Space>): Promise<void> => {
|
||||
await client.createDoc(preference.class.SpacePreference, preference.space.Preference, {
|
||||
attachedTo: _id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
|
||||
@@ -62,12 +73,12 @@
|
||||
return editorMixin.editor
|
||||
}
|
||||
|
||||
function selectSpace(id: Ref<Space>, spaceSpecial?: string) {
|
||||
function selectSpace (id: Ref<Space>, spaceSpecial?: string) {
|
||||
dispatch('space', { space: id, spaceSpecial })
|
||||
}
|
||||
|
||||
async function getActions(space: Space): Promise<Action[]> {
|
||||
const result = [editSpace]
|
||||
async function getActions (space: Space): Promise<Action[]> {
|
||||
const result = [editSpace, starSpace]
|
||||
|
||||
const extraActions = await getContributedActions(client, space, core.class.Space)
|
||||
for (const act of extraActions) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Doc, Ref, Space } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import notification from '@anticrm/notification'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { getResource, IntlString } from '@anticrm/platform'
|
||||
import preference from '@anticrm/preference'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Action, IconEdit } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { getActions as getContributedActions } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { classIcon } from '../../utils'
|
||||
import TreeItem from './TreeItem.svelte'
|
||||
import TreeNode from './TreeNode.svelte'
|
||||
|
||||
export let label: IntlString
|
||||
export let currentSpace: Ref<Space> | undefined
|
||||
export let spaces: Space[]
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const unStarSpace: Action = {
|
||||
label: preference.string.Unstar,
|
||||
icon: preference.icon.Star,
|
||||
action: async (_id: Ref<Doc>): Promise<void> => {
|
||||
const current = await client.findOne(preference.class.SpacePreference, { attachedTo: _id as Ref<Space> })
|
||||
if (current !== undefined) {
|
||||
await client.remove(current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unStarAll: Action = {
|
||||
label: preference.string.DeleteStarred,
|
||||
icon: view.icon.Delete,
|
||||
action: async (): Promise<void> => {
|
||||
const ids = spaces.map((space) => space._id)
|
||||
const current = await client.findAll(preference.class.SpacePreference, { attachedTo: { $in: ids } })
|
||||
|
||||
await Promise.all(current.map(async (item) => { await client.remove(item) }))
|
||||
}
|
||||
}
|
||||
|
||||
function selectSpace (id: Ref<Space>, spaceSpecial?: string) {
|
||||
dispatch('space', { space: id, spaceSpecial })
|
||||
}
|
||||
|
||||
async function getActions (space: Space): Promise<Action[]> {
|
||||
const result = [unStarSpace]
|
||||
|
||||
const extraActions = await getContributedActions(client, space, core.class.Space)
|
||||
for (const act of extraActions) {
|
||||
result.push({
|
||||
icon: act.icon ?? IconEdit,
|
||||
label: act.label,
|
||||
action: async () => {
|
||||
const impl = await getResource(act.action)
|
||||
await impl(space)
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const lastViews = notificationClient.getLastViews()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
function isChanged (space: Space, lastViews: Map<Ref<Doc>, number>): boolean {
|
||||
const clazz = hierarchy.getClass(space._class)
|
||||
const lastEditMixin = hierarchy.as(clazz, notification.mixin.SpaceLastEdit)
|
||||
const field = lastEditMixin?.lastEditField
|
||||
const lastView = lastViews.get(space._id)
|
||||
if (lastView === undefined || lastView === -1) return false
|
||||
if (field === undefined) return false
|
||||
const value = (space as any)[field]
|
||||
if (isNaN(value)) return false
|
||||
|
||||
return lastView < value
|
||||
}
|
||||
</script>
|
||||
|
||||
<TreeNode label={label} parent actions={async () => [unStarAll]} indent={'ml-2'}>
|
||||
{#each spaces as space (space._id)}
|
||||
<TreeItem
|
||||
indent={'ml-4'}
|
||||
_id={space._id}
|
||||
title={space.name}
|
||||
icon={classIcon(client, space._class)}
|
||||
selected={currentSpace === space._id}
|
||||
actions={() => getActions(space)}
|
||||
bold={isChanged(space, $lastViews)}
|
||||
on:click={() => {
|
||||
selectSpace(space._id)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</TreeNode>
|
||||
Reference in New Issue
Block a user