Merge branch 'main' into ano/attachments-initial

# Conflicts:
#	plugins/board-resources/src/components/editor/CardActions.svelte
This commit is contained in:
Anna No
2022-04-14 21:26:30 +07:00
123 changed files with 2232 additions and 491 deletions
@@ -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
+4
View File
@@ -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"
}
}
+4
View File
@@ -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>
+22 -2
View File
@@ -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
}
})
+8 -2
View File
@@ -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
}
})
+1
View File
@@ -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()
+7
View File
@@ -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

+18
View File
@@ -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"
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"DeleteStarred": "Delete starred",
"Starred": "Starred",
"Star": "Star",
"Unstar": "Unstar"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"string": {
"DeleteStarred": "Удалить закладки",
"Starred": "Закладки",
"Star": "Добавить в закладки",
"Unstar": "Удалить из закладок"
}
}
+33
View File
@@ -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"
}
}
+24
View File
@@ -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`))
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
"declaration": true,
"outDir": "./lib",
"strict": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
]
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+4
View File
@@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/
+18
View File
@@ -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"
}
+33
View File
@@ -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"
}
}
+66
View File
@@ -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
+9
View File
@@ -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 } })
}}
+7 -2
View File
@@ -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}
+2
View File
@@ -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
}
+6
View File
@@ -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
+1
View File
@@ -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>