Chunter: Channel attributes (#1334)

Signed-off-by: Ruslan Izhitsky <ruslan.izhitskiy@xored.com>
This commit is contained in:
Ruslan Izhitsky 2022-04-12 12:59:38 +07:00 committed by GitHub
parent a7a049c78f
commit 80a1d0c7ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 618 additions and 139 deletions

View File

@ -1,5 +1,5 @@
//
// Copyright © 2022 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

View File

@ -18,7 +18,7 @@ import type { Backlink, Channel, ChunterMessage, Comment, Message, ThreadMessage
import contact, { Employee } from '@anticrm/contact'
import type { Account, Class, Doc, Domain, Ref, Space, Timestamp } from '@anticrm/core'
import { IndexKind } from '@anticrm/core'
import { ArrOf, Builder, Collection, Index, Model, Prop, TypeMarkup, TypeRef, TypeTimestamp, UX } from '@anticrm/model'
import { ArrOf, Builder, Collection, Index, Model, Prop, TypeMarkup, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import core, { TAttachedDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view'
@ -34,6 +34,10 @@ export const DOMAIN_COMMENT = 'comment' as Domain
export class TChannel extends TSpace implements Channel {
@Prop(TypeTimestamp(), chunter.string.LastMessage)
lastMessage?: Timestamp
@Prop(TypeString(), chunter.string.Topic)
@Index(IndexKind.FullText)
topic?: string
}
@Model(chunter.class.ChunterMessage, core.class.AttachedDoc, DOMAIN_CHUNTER)
@ -106,6 +110,14 @@ export function createModel (builder: Builder): void {
lastEditField: 'lastMessage'
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectEditor, {
editor: chunter.component.EditChannel
})
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.SpaceHeader, {
header: chunter.component.ChannelHeader
})
builder.createDoc(view.class.ViewletDescriptor, core.space.Model, {
label: chunter.string.Chat,
icon: view.icon.Table,

View File

@ -34,6 +34,7 @@ export async function createGeneral (tx: TxOperations): Promise<void> {
await tx.createDoc(chunter.class.Channel, core.space.Space, {
name: 'general',
description: 'General Channel',
topic: 'General Channel',
private: false,
archived: false,
members: []
@ -49,6 +50,7 @@ export async function createRandom (tx: TxOperations): Promise<void> {
await tx.createDoc(chunter.class.Channel, core.space.Space, {
name: 'random',
description: 'Random Talks',
topic: 'Random Talks',
private: false,
archived: false,
members: []

View File

@ -29,6 +29,7 @@ import type {
ObjectEditorHeader,
ObjectFactory,
ObjectValidator,
SpaceHeader,
Viewlet,
HTMLPresenter,
TextPresenter,
@ -58,6 +59,11 @@ export class TObjectEditorHeader extends TClass implements ObjectEditorHeader {
editor!: AnyComponent
}
@Mixin(view.mixin.SpaceHeader, core.class.Class)
export class TSpaceHeader extends TClass implements SpaceHeader {
header!: AnyComponent
}
@Mixin(view.mixin.ObjectValidator, core.class.Class)
export class TObjectValidator extends TClass implements ObjectValidator {
validator!: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status<{}>>>
@ -123,6 +129,7 @@ export function createModel (builder: Builder): void {
TObjectFactory,
TObjectEditorHeader,
THTMLPresenter,
TSpaceHeader,
TTextPresenter,
TIgnoreActions
)

View File

@ -11,6 +11,7 @@
"AddSocialLinks": "Add social links",
"Change": "Change",
"Remove": "Remove",
"Members": "Members",
"Search": "Search...",
"Unassigned": "Unassigned"
}

View File

@ -11,6 +11,7 @@
"AddSocialLinks": "Добавить контактную информацию",
"Change": "Изменить",
"Remove": "Удалить",
"Members": "Участники",
"Search": "Поиск...",
"Unassigned": "Не назначен"
}

View File

@ -0,0 +1,36 @@
<!--
// 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 { Class, Doc, Ref, Space } from '@anticrm/core'
// import { getClient } from '@anticrm/presentation'
import { Label } from '@anticrm/ui'
// import { Table } from '@anticrm/view-resources'
import presentation from '../plugin'
// const client = getClient()
</script>
<div class="flex-col">
<div class="flex-row-center">
<span class="title"><Label label={presentation.string.Members} /></span>
</div>
<!-- TODO: implement Members -->
</div>
<style lang="scss">
</style>

View File

@ -23,6 +23,7 @@ export { default as AttributesBar } from './components/AttributesBar.svelte'
export { default as Avatar } from './components/Avatar.svelte'
export { default as Card } from './components/Card.svelte'
export { default as EditableAvatar } from './components/EditableAvatar.svelte'
export { default as Members } from './components/Members.svelte'
export { default as MessageBox } from './components/MessageBox.svelte'
export { default as MessageViewer } from './components/MessageViewer.svelte'
export { default as PDFViewer } from './components/PDFViewer.svelte'

View File

@ -40,6 +40,7 @@ export default plugin(presentationId, {
AddSocialLinks: '' as IntlString,
Change: '' as IntlString,
Remove: '' as IntlString,
Members: '' as IntlString,
Search: '' as IntlString,
Unassigned: '' as IntlString
},

View File

@ -13,6 +13,7 @@
"EditUpdate": "Save...",
"EditCancel": "Cancel",
"Comments" : "Comments",
"Members": "Members",
"MentionedIn": "mentioned this ",
"ContactInfo": "Contact Info",
"Content": "Content",
@ -24,6 +25,7 @@
"Replies": "Replies",
"LastReply": "Last reply",
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
"Topic": "Topic",
"Thread": "Thread",
"New": "New",
"MarkUnread": "Mark unread",

View File

@ -13,6 +13,7 @@
"EditUpdate": "Сохранить...",
"EditCancel": "Отменить",
"Comments" : "Комментарии",
"Members": "Участники",
"MentionedIn": "упомянул(а) ",
"ContactInfo": "Контактная информация",
"Content": "Содержимое",

View File

@ -1,9 +1,12 @@
import { Backlink, Comment } from '@anticrm/chunter'
import { Backlink } from '@anticrm/chunter'
import contact, { EmployeeAccount } from '@anticrm/contact'
import { Account, Class, Client, Data, Doc, DocumentQuery, Ref, TxOperations } from '@anticrm/core'
import chunter from './plugin'
export async function getUser (client: Client, user: Ref<EmployeeAccount> | Ref<Account>): Promise<EmployeeAccount | undefined> {
export async function getUser (
client: Client,
user: Ref<EmployeeAccount> | Ref<Account>
): Promise<EmployeeAccount | undefined> {
return await client.findOne(contact.class.EmployeeAccount, { _id: user as Ref<EmployeeAccount> })
}
@ -23,10 +26,20 @@ export function getTime (time: number): string {
export function isToday (time: number): boolean {
const current = new Date()
const target = new Date(time)
return current.getDate() === target.getDate() && current.getMonth() === target.getMonth() && current.getFullYear() === target.getFullYear()
return (
current.getDate() === target.getDate() &&
current.getMonth() === target.getMonth() &&
current.getFullYear() === target.getFullYear()
)
}
function extractBacklinks (backlinkId: Ref<Doc>, backlinkClass: Ref<Class<Doc>>, attachedDocId: Ref<Doc> | undefined, message: string, kids: NodeListOf<ChildNode>): Array<Data<Backlink>> {
function extractBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
message: string,
kids: NodeListOf<ChildNode>
): Array<Data<Backlink>> {
const result: Array<Data<Backlink>> = []
const nodes: Array<NodeListOf<ChildNode>> = [kids]
@ -40,7 +53,7 @@ function extractBacklinks (backlinkId: Ref<Doc>, backlinkClass: Ref<Class<Doc>>,
const el = kid as HTMLElement
const ato = el.getAttribute('data-id') as Ref<Doc>
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
const e = result.find(e => e.attachedTo === ato && e.attachedToClass === atoClass)
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined) {
result.push({
attachedTo: ato,
@ -59,20 +72,44 @@ function extractBacklinks (backlinkId: Ref<Doc>, backlinkClass: Ref<Class<Doc>>,
return result
}
export function getBacklinks (backlinkId: Ref<Doc>, backlinkClass: Ref<Class<Doc>>, attachedDocId: Ref<Doc> | undefined, content: string): Array<Data<Backlink>> {
export function getBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Array<Data<Backlink>> {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'application/xhtml+xml')
return extractBacklinks(backlinkId, backlinkClass, attachedDocId, content, doc.childNodes as NodeListOf<HTMLElement>)
}
export async function createBacklinks (client: TxOperations, backlinkId: Ref<Doc>, backlinkClass: Ref<Class<Doc>>, attachedDocId: Ref<Doc> | undefined, content: string): Promise<void> {
export async function createBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Promise<void> {
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
for (const backlink of backlinks) {
const { attachedTo, attachedToClass, collection, ...adata } = backlink
await client.addCollection(chunter.class.Backlink, chunter.space.Backlinks, attachedTo, attachedToClass, collection, adata)
await client.addCollection(
chunter.class.Backlink,
chunter.space.Backlinks,
attachedTo,
attachedToClass,
collection,
adata
)
}
}
export async function updateBacklinks (client: TxOperations, backlinkId: Ref<Doc>, backlinkClass: Ref<Class<Doc>>, attachedDocId: Ref<Doc> | undefined, content: string): Promise<void> {
export async function updateBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Promise<void> {
const q: DocumentQuery<Backlink> = { backlinkId, backlinkClass }
if (attachedDocId !== undefined) {
q.attachedDocId = attachedDocId
@ -83,12 +120,14 @@ export async function updateBacklinks (client: TxOperations, backlinkId: Ref<Doc
// We need to find ones we need to remove, and ones we need to update.
for (const c of current) {
// Find existing and check if we need to update message.
const pos = backlinks.findIndex(b => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass)
const pos = backlinks.findIndex((b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass)
if (pos !== -1) {
// We need to check and update if required.
const data = backlinks[pos]
if (c.message !== data.message) {
await client.updateCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection, { message: data.message })
await client.updateCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection, {
message: data.message
})
}
backlinks.splice(pos, 1)
} else {
@ -99,6 +138,13 @@ export async function updateBacklinks (client: TxOperations, backlinkId: Ref<Doc
// Add missing backlinks
for (const backlink of backlinks) {
const { attachedTo, attachedToClass, collection, ...adata } = backlink
await client.addCollection(chunter.class.Backlink, chunter.space.Backlinks, attachedTo, attachedToClass, collection, adata)
await client.addCollection(
chunter.class.Backlink,
chunter.space.Backlinks,
attachedTo,
attachedToClass,
collection,
adata
)
}
}

View File

@ -15,11 +15,11 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import type { Message } from '@anticrm/chunter'
import contact,{ Employee } from '@anticrm/contact'
import core,{ Doc,Ref,Space,WithLookup } from '@anticrm/core'
import contact, { Employee } from '@anticrm/contact'
import core, { Doc, Ref, Space, WithLookup } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { createQuery } from '@anticrm/presentation'
import { afterUpdate,beforeUpdate } from 'svelte'
import { afterUpdate, beforeUpdate } from 'svelte'
import chunter from '../plugin'
import ChannelSeparator from './ChannelSeparator.svelte'
import MessageComponent from './Message.svelte'

View File

@ -0,0 +1,49 @@
<!--
// 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 { Channel } from '@anticrm/chunter'
import type { Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { showPanel } from '@anticrm/ui'
import chunter from '../plugin'
import { classIcon } from '../utils'
import Header from './Header.svelte'
export let spaceId: Ref<Channel> | undefined
const client = getClient()
const query = createQuery()
let channel: Channel | undefined
$: query.query(chunter.class.Channel, { _id: spaceId }, (result) => {
channel = result[0]
})
async function onSpaceEdit (): Promise<void> {
if (channel === undefined) return
showPanel(chunter.component.EditChannel, channel._id, channel._class, 'right')
}
</script>
<div class="ac-header divide full">
{#if channel}
<Header
icon={classIcon(client, channel._class)}
label={channel.name}
description={channel.topic}
on:click={onSpaceEdit}
/>
{/if}
</div>

View File

@ -12,10 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { IntlString } from "@anticrm/platform"
import { Label } from "@anticrm/ui"
import type { IntlString } from '@anticrm/platform'
import { Label } from '@anticrm/ui'
export let title: IntlString
export let line: boolean = false
@ -24,9 +23,9 @@
export let isNew: boolean = false
</script>
<div class="w-full text-sm flex-center whitespace-nowrap mb-6" class:flex-reverse={reverse} class:new={isNew} >
<div class="w-full text-sm flex-center whitespace-nowrap mb-6" class:flex-reverse={reverse} class:new={isNew}>
<Label label={title} {params} />
<div class:ml-4={!reverse} class:mr-4={reverse} class:line={line} ></div>
<div class:ml-4={!reverse} class:mr-4={reverse} class:line />
</div>
<style lang="scss">
@ -35,7 +34,6 @@
width: 100%;
height: 1px;
background-color: var(--theme-chat-divider);
}
.new {
.line {

View File

@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import { Message } from '@anticrm/chunter'
import { generateId,getCurrentAccount,Ref,Space, TxFactory } from '@anticrm/core'
import { generateId, getCurrentAccount, Ref, Space, TxFactory } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { getClient } from '@anticrm/presentation'
import { getCurrentLocation,navigate } from '@anticrm/ui'
import { getCurrentLocation, navigate } from '@anticrm/ui'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import Channel from './Channel.svelte'
@ -35,15 +34,20 @@
const { message, attachments } = event.detail
const me = getCurrentAccount()._id
const txFactory = new TxFactory(me)
const tx = txFactory.createTxCreateDoc<Message>(_class, space, {
attachedTo: space,
attachedToClass: chunter.class.Channel,
collection: 'messages',
content: message,
createOn: 0,
createBy: me,
attachments
}, _id)
const tx = txFactory.createTxCreateDoc<Message>(
_class,
space,
{
attachedTo: space,
attachedToClass: chunter.class.Channel,
collection: 'messages',
content: message,
createOn: 0,
createBy: me,
attachments
},
_id
)
tx.attributes.createOn = tx.modifiedOn
await notificationClient.updateLastView(space, chunter.class.Channel, tx.modifiedOn, true)
await client.tx(tx)
@ -59,12 +63,16 @@
loc.path[3] = _id
navigate(loc)
}
</script>
<Channel {space} on:openThread={(e) => { openThread(e.detail) }} />
<Channel
{space}
on:openThread={(e) => {
openThread(e.detail)
}}
/>
<div class="reference">
<AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage}/>
<AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage} />
</div>
<style lang="scss">

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Person } from '@anticrm/contact'
import { formatName } from '@anticrm/contact'
@ -25,7 +24,6 @@
export let user: Person
export let message: IMessage
</script>
<div class="flex-nowrap">
@ -44,16 +42,16 @@
margin-right: 1.25rem;
.header {
margin-bottom: .25rem;
margin-bottom: 0.25rem;
font-weight: 500;
font-size: 1rem;
line-height: 150%;
color: var(--theme-caption-color);
span {
margin-left: .5rem;
margin-left: 0.5rem;
font-weight: 400;
font-size: .875rem;
font-size: 0.875rem;
color: var(--theme-content-dark-color);
}
}

View File

@ -1,4 +1,3 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
@ -26,15 +25,23 @@
export let object: Doc
const _class = chunter.class.Comment
let _id: Ref<Comment> = generateId()
async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail
await client.addCollection<Doc, Comment>(_class, object.space, object._id, object._class, 'comments', { message, attachments }, _id)
await client.addCollection<Doc, Comment>(
_class,
object.space,
object._id,
object._class,
'comments',
{ message, attachments },
_id
)
// Create an backlink to document
await createBacklinks(client, object._id, object._class, _id, message)
_id = generateId()
}
</script>
<AttachmentRefInput {_class} space={object.space} objectId={_id} on:message={onMessage}/>
<AttachmentRefInput {_class} space={object.space} objectId={_id} on:message={onMessage} />

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Ref, Doc, SortingOrder } from '@anticrm/core'
@ -25,10 +24,14 @@
let comments: Comment[] = []
const query = createQuery()
$: query.query(chunter.class.Comment, { attachedTo: objectId }, (res) => {
comments = res
}, { limit: 3, sort: { modifiedOn: SortingOrder.Descending } })
$: query.query(
chunter.class.Comment,
{ attachedTo: objectId },
(res) => {
comments = res
},
{ limit: 3, sort: { modifiedOn: SortingOrder.Descending } }
)
</script>
{#each comments as comment}
@ -38,6 +41,10 @@
{/each}
<style lang="scss">
.item { max-width: 30rem; }
.item + .item { margin-top: 1.25rem; }
.item {
max-width: 30rem;
}
.item + .item {
margin-top: 1.25rem;
}
</style>

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { AttachmentDocList } from '@anticrm/attachment-resources'
import type { Comment } from '@anticrm/chunter'
@ -41,7 +40,7 @@
<div class="content-trans-color ml-4"><TimeSince value={value.modifiedOn} /></div>
</div>
<ShowMore limit={126} fixed>
<MessageViewer message={value.message}/>
<MessageViewer message={value.message} />
<AttachmentDocList {value} />
</ShowMore>
</div>
@ -56,6 +55,6 @@
display: inline-flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: .25rem;
margin-bottom: 0.25rem;
}
</style>
</style>

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Doc } from '@anticrm/core'
import { Tooltip, IconThread } from '@anticrm/ui'
@ -28,9 +27,9 @@
{#if value && value.comments && value.comments > 0}
<Tooltip label={chunter.string.Comments} component={CommentPopup} props={{ objectId: value._id }}>
<div class="sm-tool-icon ml-1 mr-1">
<span class="icon"><IconThread {size}/></span>
<span class="icon"><IconThread {size} /></span>
{#if showCounter}
&nbsp;{value.comments}
&nbsp;{value.comments}
{/if}
</div>
</Tooltip>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { IconFolder, EditBox, ToggleWithLabel, Grid } from '@anticrm/ui'
@ -25,16 +24,15 @@
const dispatch = createEventDispatcher()
let name: string = ''
let description: string = ''
export function canClose(): boolean {
export function canClose (): boolean {
return name === ''
}
const client = getClient()
function createChannel() {
function createChannel () {
client.createDoc(chunter.class.Channel, core.space.Space, {
name,
description,
description: '',
private: false,
archived: false,
members: [getCurrentAccount()._id]
@ -43,10 +41,12 @@
</script>
<SpaceCreateCard
label={chunter.string.CreateChannel}
label={chunter.string.CreateChannel}
okAction={createChannel}
canSave={name ? true : false}
on:close={() => { dispatch('close') }}
canSave={!!name}
on:close={() => {
dispatch('close')
}}
>
<Grid column={1} rowGap={1.5}>
<EditBox
@ -57,6 +57,6 @@
maxWidth={'16rem'}
focus
/>
<ToggleWithLabel label={chunter.string.MakePrivate} description={chunter.string.MakePrivateDescription}/>
<ToggleWithLabel label={chunter.string.MakePrivate} description={chunter.string.MakePrivateDescription} />
</Grid>
</SpaceCreateCard>

View File

@ -12,9 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Timestamp } from "@anticrm/core"
import { Timestamp } from '@anticrm/core'
export let value: Timestamp
export let line: boolean = false
@ -32,7 +31,7 @@
}
</script>
<div class="flex-center container" class:line={line}>
<div class="flex-center container" class:line>
<div class="title">{new Intl.DateTimeFormat('default', options).format(value)}</div>
</div>
@ -44,10 +43,10 @@
.title {
position: relative;
padding: .375rem .75rem;
padding: 0.375rem 0.75rem;
font-weight: 600;
font-size: .75rem;
letter-spacing: .5;
font-size: 0.75rem;
letter-spacing: 0.5;
text-transform: uppercase;
color: var(--theme-content-trans-color);
z-index: 1;

View File

@ -0,0 +1,91 @@
<!--
//
// 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 { Channel } from '@anticrm/chunter'
import type { Class, Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { createQuery, getClient, Members } from '@anticrm/presentation'
import { Icon, IconClose, Label, ActionIcon, Scroller } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import chunter from '../plugin'
import EditChannelDescriptionTab from './EditChannelDescriptionTab.svelte'
export let _id: Ref<Channel>
export let _class: Ref<Class<Channel>>
let channel: Channel
const dispatch = createEventDispatcher()
const client = getClient()
const clazz = client.getHierarchy().getClass(_class)
const query = createQuery()
$: query.query(chunter.class.Channel, { _id }, (result) => {
channel = result[0]
})
const tabLabels: IntlString[] = [chunter.string.Channel, chunter.string.Members]
let selectedTabIndex = 0
</script>
<div
on:click={() => {
dispatch('close')
}}
/>
<div class="antiDialogs antiComponent">
<div class="ac-header short mirror divide">
<div class="ac-header__wrap-title">
<div class="ac-header__icon">
{#if clazz.icon}<Icon icon={clazz.icon} size={'medium'} />{/if}
</div>
<div class="ac-header__title"><Label label={clazz.label} /></div>
</div>
<div class="tool">
<ActionIcon
icon={IconClose}
size={'small'}
action={() => {
dispatch('close')
}}
/>
</div>
</div>
<div class="ac-tabs">
{#each tabLabels as tabLabel, i}
<div
class="ac-tabs__tab"
class:selected={i === selectedTabIndex}
on:click={() => {
selectedTabIndex = i
}}
>
<Label label={tabLabel} />
</div>
{/each}
<div class="ac-tabs__empty" />
</div>
<Scroller padding>
{#if selectedTabIndex === 0}
<EditChannelDescriptionTab {channel} {_id} {_class} />
{:else}
<!-- Channel members -->
<Members />
{/if}
</Scroller>
</div>

View File

@ -0,0 +1,86 @@
<!--
//
// 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 { Channel } from '@anticrm/chunter'
import type { Class, Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { EditBox } from '@anticrm/ui'
import chunter from '../plugin'
export let _id: Ref<Channel>
export let _class: Ref<Class<Channel>>
export let channel: Channel
const client = getClient()
const clazz = client.getHierarchy().getClass(_class)
const query = createQuery()
function onNameChange (ev: Event) {
const value = (ev.target as HTMLInputElement).value
if (value.trim().length > 0) {
client.updateDoc(_class, channel.space, channel._id, { name: value })
} else {
// Just refresh value
query.query(chunter.class.Channel, { _id }, (result) => {
channel = result[0]
})
}
}
function onTopicChange (ev: Event) {
const newTopic = (ev.target as HTMLInputElement).value
client.update(channel, { topic: newTopic })
}
function onDescriptionChange (ev: Event) {
const newDescription = (ev.target as HTMLInputElement).value
client.update(channel, { description: newDescription })
}
</script>
{#if channel}
<div class="flex-col flex-gap-3">
<EditBox
label={clazz.label}
icon={clazz.icon}
bind:value={channel.name}
placeholder={clazz.label}
maxWidth="39rem"
focus
on:change={onNameChange}
/>
<EditBox
label={chunter.string.Topic}
bind:value={channel.topic}
placeholder={chunter.string.Topic}
maxWidth="39rem"
focus
on:change={onTopicChange}
/>
<EditBox
label={chunter.string.ChannelDescription}
bind:value={channel.description}
placeholder={chunter.string.ChannelDescription}
maxWidth="39rem"
focus
on:change={onDescriptionChange}
/>
<!-- TODO: implement Attachments here -->
</div>
{/if}

View File

@ -0,0 +1,39 @@
<!--
// 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 { Asset } from '@anticrm/platform'
import { Icon } from '@anticrm/ui'
export let icon: Asset | undefined
export let label: string
export let description: string | undefined
</script>
<div class="ac-header__wrap-description">
<div class="ac-header__wrap-title" on:click>
{#if icon}<div class="ac-header__icon"><Icon {icon} size={'small'} /></div>{/if}
<span class="ac-header__title">{label}</span>
</div>
{#if description}<span class="ac-header__description">{description}</span>{/if}
</div>
<style lang="scss">
.ac-header__wrap-title:hover {
cursor: pointer;
span {
text-decoration: underline;
}
}
</style>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { AnySvelteComponent } from '@anticrm/ui'
import Check from './icons/Check.svelte'
@ -23,13 +22,16 @@
count: number
}
export let reactions: Reaction[] = [{ icon: Check, count: 3}, { icon: Heart, count: 10}]
export let reactions: Reaction[] = [
{ icon: Check, count: 3 },
{ icon: Heart, count: 10 }
]
</script>
<div class="container">
{#each reactions as reaction}
<div class="flex-row-center reaction">
<svelte:component this={reaction.icon} size={'medium'}/>
<svelte:component this={reaction.icon} size={'medium'} />
<div class="caption-color counter">{reaction.count}</div>
</div>
{/each}
@ -40,7 +42,11 @@
display: flex;
user-select: none;
.counter { margin-left: .25rem; }
.reaction + .reaction { margin-left: 1rem; }
.counter {
margin-left: 0.25rem;
}
.reaction + .reaction {
margin-left: 1rem;
}
}
</style>
</style>

View File

@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact,{ Employee } from '@anticrm/contact'
import contact, { Employee } from '@anticrm/contact'
import { Ref, Timestamp } from '@anticrm/core'
import { Avatar,createQuery } from '@anticrm/presentation'
import { Avatar, createQuery } from '@anticrm/presentation'
import { Label, TimeSince } from '@anticrm/ui'
import chunter from '../plugin'
@ -32,16 +31,19 @@
$: updateQuery(employees)
function updateQuery (employees: Set<Ref<Employee>>) {
query.query(contact.class.Employee, {
_id: { $in: Array.from(employees) }
}, (res) => {
showReplies = res
}, {
limit: shown
})
query.query(
contact.class.Employee,
{
_id: { $in: Array.from(employees) }
},
(res) => {
showReplies = res
},
{
limit: shown
}
)
}
</script>
<div class="flex-row-center container cursor-pointer" on:click>
@ -53,7 +55,9 @@
<div class="reply"><span>+{employees.size - shown}</span></div>
{/if}
</div>
<div class="whitespace-nowrap ml-2 mr-2 over-underline"><Label label={chunter.string.RepliesCount} params={{ replies: replies.length }} /></div>
<div class="whitespace-nowrap ml-2 mr-2 over-underline">
<Label label={chunter.string.RepliesCount} params={{ replies: replies.length }} />
</div>
{#if replies.length > 1}
<div class="mr-1">
<Label label={chunter.string.LastReply} />
@ -82,9 +86,9 @@
align-items: center;
width: 1.5rem;
height: 1.5rem;
font-size: .75rem;
font-size: 0.75rem;
font-weight: 500;
line-height: .5;
line-height: 0.5;
color: var(--theme-caption-color);
background-color: var(--theme-bg-selection);
border-radius: 50%;
@ -100,4 +104,4 @@
background-color: var(--theme-bg-color);
}
}
</style>
</style>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Backlink } from '@anticrm/chunter'
import { MessageViewer } from '@anticrm/presentation'
@ -22,4 +21,4 @@
// export let edit: boolean = false
</script>
<MessageViewer message={value.message}/>
<MessageViewer message={value.message} />

View File

@ -12,13 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Comment } from '@anticrm/chunter'
import type { TxCreateDoc } from '@anticrm/core'
import { getClient, MessageViewer } from '@anticrm/presentation'
import { AttachmentDocList } from '@anticrm/attachment-resources'
import { ReferenceInput } from '@anticrm/text-editor'
import { Button } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { updateBacklinks } from '../../backlinks'
@ -36,13 +34,21 @@
async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail
await client.updateCollection(tx.objectClass, tx.objectSpace, tx.objectId, value.attachedTo, value.attachedToClass, value.collection, {
message,
attachments
})
await client.updateCollection(
tx.objectClass,
tx.objectSpace,
tx.objectId,
value.attachedTo,
value.attachedToClass,
value.collection,
{
message,
attachments
}
)
// We need to update backlinks before and after.
await updateBacklinks(client, value.attachedTo, value.attachedToClass, value._id, event.detail)
dispatch('close', false)
}
let refInput: AttachmentRefInput
@ -50,19 +56,32 @@
<div class:editing>
{#if edit}
<AttachmentRefInput bind:this={refInput} _class={value._class} objectId={value._id} space={value.space} content={value.message} on:message={onMessage} showSend={false} />
<div class='flex-row-reverse gap-2 reverse'>
<Button label={chunter.string.EditCancel} on:click={() => {
dispatch('close', false)
}}/>
<AttachmentRefInput
bind:this={refInput}
_class={value._class}
objectId={value._id}
space={value.space}
content={value.message}
on:message={onMessage}
showSend={false}
/>
<div class="flex-row-reverse gap-2 reverse">
<Button
label={chunter.string.EditCancel}
on:click={() => {
dispatch('close', false)
}}
/>
<Button label={chunter.string.EditUpdate} on:click={() => refInput.submit()} />
</div>
{:else}
<MessageViewer message={value.message}/>
<MessageViewer message={value.message} />
<AttachmentDocList {value} />
{/if}
</div>
<style lang="scss">
.editing { border: 1px solid var(--primary-button-focused-border); }
.editing {
border: 1px solid var(--primary-button-focused-border);
}
</style>

View File

@ -4,5 +4,7 @@
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M16.2,18c-0.1,0-0.2,0-0.3-0.1l-6-4l-6,4c-0.2,0.1-0.4,0.1-0.5,0c-0.2-0.1-0.3-0.3-0.3-0.4V4.2 C3.2,3,4.3,2,5.5,2h8.9c1.3,0,2.3,1,2.3,2.2v13.3c0,0.2-0.1,0.4-0.3,0.4C16.4,18,16.3,18,16.2,18z M10,12.8c0.1,0,0.2,0,0.3,0.1 l5.5,3.6V4.2c0-0.6-0.6-1.2-1.3-1.2H5.5C4.8,3,4.2,3.5,4.2,4.2v12.4l5.5-3.6C9.8,12.9,9.9,12.8,10,12.8z"/>
<path
d="M16.2,18c-0.1,0-0.2,0-0.3-0.1l-6-4l-6,4c-0.2,0.1-0.4,0.1-0.5,0c-0.2-0.1-0.3-0.3-0.3-0.4V4.2 C3.2,3,4.3,2,5.5,2h8.9c1.3,0,2.3,1,2.3,2.2v13.3c0,0.2-0.1,0.4-0.3,0.4C16.4,18,16.3,18,16.2,18z M10,12.8c0.1,0,0.2,0,0.3,0.1 l5.5,3.6V4.2c0-0.6-0.6-1.2-1.3-1.2H5.5C4.8,3,4.2,3.5,4.2,4.2v12.4l5.5-3.6C9.8,12.9,9.9,12.8,10,12.8z"
/>
</svg>

View File

@ -3,6 +3,6 @@
</script>
<svg class="svg-{size}" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill={'#27B166'} d="M10.1,1C5.2,0.9,1.1,4.9,1,9.8V10c0,5,4,9,9,9c4.9,0,9-4,9-8.9C19,5.1,15.1,1.1,10.1,1z"/>
<polygon fill={'#FCFCFC'} points="8.5,14.3 5.4,11.1 6.6,9.9 8.5,11.7 13.9,6.4 15.1,7.6 "/>
<path fill={'#27B166'} d="M10.1,1C5.2,0.9,1.1,4.9,1,9.8V10c0,5,4,9,9,9c4.9,0,9-4,9-8.9C19,5.1,15.1,1.1,10.1,1z" />
<polygon fill={'#FCFCFC'} points="8.5,14.3 5.4,11.1 6.6,9.9 8.5,11.7 13.9,6.4 15.1,7.6 " />
</svg>

View File

@ -4,5 +4,7 @@
</script>
<svg class="svg-{size}" viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<path d="M12.8,12l7.6-7.6c0.2-0.2,0.2-0.6,0-0.8s-0.6-0.2-0.8,0L12,11.2L4.4,3.6c-0.2-0.2-0.6-0.2-0.8,0s-0.2,0.6,0,0.8 l7.6,7.6l-7.6,7.6c-0.2,0.2-0.2,0.6,0,0.8c0.1,0.1,0.3,0.2,0.4,0.2s0.3-0.1,0.4-0.2l7.6-7.6l7.6,7.6c0.1,0.1,0.3,0.2,0.4,0.2 s0.3-0.1,0.4-0.2c0.2-0.2,0.2-0.6,0-0.8L12.8,12z"/>
<path
d="M12.8,12l7.6-7.6c0.2-0.2,0.2-0.6,0-0.8s-0.6-0.2-0.8,0L12,11.2L4.4,3.6c-0.2-0.2-0.6-0.2-0.8,0s-0.2,0.6,0,0.8 l7.6,7.6l-7.6,7.6c-0.2,0.2-0.2,0.6,0,0.8c0.1,0.1,0.3,0.2,0.4,0.2s0.3-0.1,0.4-0.2l7.6-7.6l7.6,7.6c0.1,0.1,0.3,0.2,0.4,0.2 s0.3-0.1,0.4-0.2c0.2-0.2,0.2-0.6,0-0.8L12.8,12z"
/>
</svg>

View File

@ -13,12 +13,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'var(--theme-caption-color)'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM7.15467 12.4273C8.66416 13.9463 11.0877 14.0045 12.6671 12.5961L12.8453 12.4273C13.04 12.2314 13.3566 12.2304 13.5524 12.4251C13.7265 12.5981 13.7467 12.8674 13.6123 13.0627L13.5547 13.1322L13.5323 13.1545C11.5691 15.1054 8.39616 15.0953 6.44533 13.1322C6.25069 12.9363 6.25169 12.6197 6.44757 12.4251C6.64344 12.2304 6.96002 12.2314 7.15467 12.4273ZM12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5ZM7.5 7.5C8.05228 7.5 8.5 7.94772 8.5 8.5C8.5 9.05228 8.05228 9.5 7.5 9.5C6.94772 9.5 6.5 9.05228 6.5 8.5C6.5 7.94772 6.94772 7.5 7.5 7.5Z"/>
<path
d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM7.15467 12.4273C8.66416 13.9463 11.0877 14.0045 12.6671 12.5961L12.8453 12.4273C13.04 12.2314 13.3566 12.2304 13.5524 12.4251C13.7265 12.5981 13.7467 12.8674 13.6123 13.0627L13.5547 13.1322L13.5323 13.1545C11.5691 15.1054 8.39616 15.0953 6.44533 13.1322C6.25069 12.9363 6.25169 12.6197 6.44757 12.4251C6.64344 12.2304 6.96002 12.2314 7.15467 12.4273ZM12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5ZM7.5 7.5C8.05228 7.5 8.5 7.94772 8.5 8.5C8.5 9.05228 8.05228 9.5 7.5 9.5C6.94772 9.5 6.5 9.05228 6.5 8.5C6.5 7.94772 6.94772 7.5 7.5 7.5Z"
/>
</svg>

View File

@ -3,5 +3,8 @@
</script>
<svg class="svg-{size}" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill={'#F96E50'} d="M1,6.8c0-1.3,0.5-2.6,1.5-3.5c0.9-0.9,2.2-1.4,3.5-1.4c1.6,0,3,0.6,4.1,1.8c1-1.2,2.5-1.8,4.1-1.8 c1.3,0,2.6,0.5,3.5,1.4C18.5,4.2,19,5.5,19,6.8c0,4.8-5.8,8.5-9,11.3C6.7,15.2,1,11.6,1,6.8z"/>
<path
fill={'#F96E50'}
d="M1,6.8c0-1.3,0.5-2.6,1.5-3.5c0.9-0.9,2.2-1.4,3.5-1.4c1.6,0,3,0.6,4.1,1.8c1-1.2,2.5-1.8,4.1-1.8 c1.3,0,2.6,0.5,3.5,1.4C18.5,4.2,19,5.5,19,6.8c0,4.8-5.8,8.5-9,11.3C6.7,15.2,1,11.6,1,6.8z"
/>
</svg>

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import chunter, { Comment, Message, ThreadMessage } from '@anticrm/chunter'
import chunter, { Message, ThreadMessage } from '@anticrm/chunter'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { Resources } from '@anticrm/platform'
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
@ -21,10 +21,12 @@ import TxBacklinkReference from './components/activity/TxBacklinkReference.svelt
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte'
import ChannelView from './components/ChannelView.svelte'
import ChannelHeader from './components/ChannelHeader.svelte'
import CommentInput from './components/CommentInput.svelte'
import CommentPresenter from './components/CommentPresenter.svelte'
import CommentsPresenter from './components/CommentsPresenter.svelte'
import CreateChannel from './components/CreateChannel.svelte'
import EditChannel from './components/EditChannel.svelte'
import ThreadView from './components/ThreadView.svelte'
export { CommentsPresenter }
@ -64,10 +66,12 @@ export default async (): Promise<Resources> => ({
component: {
CommentInput,
CreateChannel,
ChannelHeader,
ChannelView,
CommentPresenter,
CommentsPresenter,
ChannelPresenter,
EditChannel,
ThreadView
},
activity: {

View File

@ -22,7 +22,9 @@ import type { AnyComponent } from '@anticrm/ui'
export default mergeIds(chunterId, chunter, {
component: {
CreateChannel: '' as AnyComponent,
ChannelView: '' as AnyComponent
ChannelHeader: '' as AnyComponent,
ChannelView: '' as AnyComponent,
EditChannel: '' as AnyComponent
},
actionImpl: {
SubscribeMessage: '' as Resource<(object: Doc) => Promise<void>>,
@ -39,8 +41,10 @@ export default mergeIds(chunterId, chunter, {
ChannelDescription: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString,
Members: '' as IntlString,
In: '' as IntlString,
Replies: '' as IntlString,
Topic: '' as IntlString,
Thread: '' as IntlString,
RepliesCount: '' as IntlString,
LastReply: '' as IntlString,

View File

@ -1,16 +1,20 @@
import contact, { EmployeeAccount } from '@anticrm/contact'
import { Account, Client, Ref, Timestamp } from '@anticrm/core'
import { Account, Class, Client, Obj, Ref } from '@anticrm/core'
import { Asset } from '@anticrm/platform'
export async function getUser (client: Client, user: Ref<EmployeeAccount> | Ref<Account>): Promise<EmployeeAccount | undefined> {
export async function getUser (
client: Client,
user: Ref<EmployeeAccount> | Ref<Account>
): Promise<EmployeeAccount | undefined> {
return await client.findOne(contact.class.EmployeeAccount, { _id: user as Ref<EmployeeAccount> })
}
export function getTime (time: number): string {
let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric'}
let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' }
if (!isToday(time)) {
options = {
month: 'numeric',
day: 'numeric',
day: 'numeric',
...options
}
}
@ -21,5 +25,13 @@ export function getTime (time: number): string {
export function isToday (time: number): boolean {
const current = new Date()
const target = new Date(time)
return current.getDate() === target.getDate() && current.getMonth() === target.getMonth() && current.getFullYear() === target.getFullYear()
}
return (
current.getDate() === target.getDate() &&
current.getMonth() === target.getMonth() &&
current.getFullYear() === target.getFullYear()
)
}
export function classIcon (client: Client, _class: Ref<Class<Obj>>): Asset | undefined {
return client.getHierarchy().getClass(_class).icon
}

View File

@ -24,6 +24,7 @@ import { AnyComponent } from '@anticrm/ui'
*/
export interface Channel extends Space {
lastMessage?: Timestamp
topic?: string
}
/**

View File

@ -1,5 +1,5 @@
//
// Copyright © 2022 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

View File

@ -40,6 +40,13 @@ export interface ObjectEditor extends Class<Doc> {
editor: AnyComponent
}
/**
* @public
*/
export interface SpaceHeader extends Class<Doc> {
header: AnyComponent
}
/**
* @public
*/
@ -178,6 +185,7 @@ const view = plugin(viewId, {
ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>,
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>,
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>,
SpaceHeader: '' as Ref<Mixin<SpaceHeader>>,
IgnoreActions: '' as Ref<Mixin<IgnoreActions>>,
HTMLPresenter: '' as Ref<Mixin<HTMLPresenter>>,
TextPresenter: '' as Ref<Mixin<TextPresenter>>

View File

@ -16,8 +16,8 @@
<script lang="ts">
import core, { Class, Doc, Ref, Space, WithLookup } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import type { AnyComponent } from '@anticrm/ui'
import { createQuery, getClient } from '@anticrm/presentation'
import { AnyComponent, Component } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import type { ViewConfiguration } from '@anticrm/workbench'
@ -31,14 +31,14 @@
let search: string = ''
let viewlet: WithLookup<Viewlet> | undefined = undefined
let space: Ref<Space> | undefined = undefined
let space: Space | undefined
let _class: Ref<Class<Doc>> | undefined = undefined
const client = getClient()
let viewlets: WithLookup<Viewlet>[] = []
async function update (attachTo?: Ref<Class<Doc>>, currentSpace?: Ref<Space>): Promise<void> {
async function update (attachTo?: Ref<Class<Doc>>): Promise<void> {
if (attachTo) {
viewlets = await client.findAll(view.class.Viewlet, { attachTo }, {
lookup: {
@ -47,12 +47,36 @@
})
_class = attachTo
}
space = currentSpace
}
$: update(currentView?.class, currentSpace)
$: update(currentView?.class)
const query = createQuery()
$: currentSpace && query.query(core.class.Space, {
_id: currentSpace
}, (res) => {
space = res[0]
}, {
limit: 1
})
const hierarchy = client.getHierarchy()
async function getHeader (_class: Ref<Class<Space>>): Promise<AnyComponent | undefined> {
const clazz = hierarchy.getClass(_class)
const headerMixin = hierarchy.as(clazz, view.mixin.SpaceHeader)
if (headerMixin?.header == null && clazz.extends != null) return getHeader(clazz.extends)
return headerMixin.header
}
</script>
<SpaceHeader spaceId={space} {viewlets} {createItemDialog} {createItemLabel} bind:search={search} bind:viewlet={viewlet} />
{#if _class && space}
<SpaceContent {space} {_class} {search} {viewlet} />
{#await getHeader(space._class) then header}
{#if header}
<Component is={header} props={{ spaceId: space._id, viewlets, createItemDialog, createItemLabel }} />
{:else}
<SpaceHeader spaceId={space._id} {viewlets} {createItemDialog} {createItemLabel} bind:search={search} bind:viewlet={viewlet} />
{/if}
{/await}
<SpaceContent space={space._id} {_class} {search} {viewlet} />
{/if}