Members init (#1479)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-04-21 21:47:04 +06:00 committed by GitHub
parent c1a697ee5e
commit ed0a747330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 624 additions and 186 deletions

View File

@ -14,11 +14,31 @@
//
import activity from '@anticrm/activity'
import type { Backlink, Channel, ChunterMessage, Comment, Message, SavedMessages, ThreadMessage } from '@anticrm/chunter'
import type {
Backlink,
Channel,
ChunterMessage,
Comment,
Message,
SavedMessages,
ThreadMessage
} from '@anticrm/chunter'
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, TypeString, 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'
@ -131,11 +151,16 @@ export function createModel (builder: Builder): void {
header: chunter.component.ChannelHeader
})
builder.createDoc(view.class.ViewletDescriptor, core.space.Model, {
label: chunter.string.Chat,
icon: view.icon.Table,
component: chunter.component.ChannelView
}, chunter.viewlet.Chat)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: chunter.string.Chat,
icon: view.icon.Table,
component: chunter.component.ChannelView
},
chunter.viewlet.Chat
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: chunter.class.Message,
@ -225,91 +250,130 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(workbench.class.Application, core.space.Model, {
label: chunter.string.ApplicationLabelChunter,
icon: chunter.icon.Chunter,
hidden: false,
navigatorModel: {
specials: [
{
id: 'archive',
component: workbench.component.Archive,
icon: view.icon.Archive,
label: workbench.string.Archive,
position: 'top',
visibleIf: workbench.function.HasArchiveSpaces,
spaceClass: chunter.class.Channel
},
{
id: 'threads',
label: chunter.string.Threads,
icon: chunter.icon.Thread,
component: chunter.component.Threads,
position: 'top'
},
{
id: 'savedMessages',
label: chunter.string.SavedMessages,
icon: chunter.icon.Bookmark,
component: chunter.component.SavedMessages
}
],
spaces: [
{
label: chunter.string.Channels,
spaceClass: chunter.class.Channel,
addSpaceLabel: chunter.string.CreateChannel,
createComponent: chunter.component.CreateChannel
}
],
aside: chunter.component.ThreadView
}
}, chunter.app.Chunter)
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: chunter.string.ApplicationLabelChunter,
icon: chunter.icon.Chunter,
hidden: false,
navigatorModel: {
specials: [
{
id: 'spaceBrowser',
component: workbench.component.SpaceBrowser,
icon: workbench.icon.Search,
label: chunter.string.ChannelBrowser,
position: 'top',
spaceClass: chunter.class.Channel,
componentProps: {
_class: chunter.class.Channel,
label: chunter.string.ChannelBrowser,
createItemDialog: chunter.component.CreateChannel,
createItemLabel: chunter.string.CreateChannel
}
},
{
id: 'archive',
component: workbench.component.Archive,
icon: view.icon.Archive,
label: workbench.string.Archive,
position: 'top',
visibleIf: workbench.function.HasArchiveSpaces,
spaceClass: chunter.class.Channel
},
{
id: 'threads',
label: chunter.string.Threads,
icon: chunter.icon.Thread,
component: chunter.component.Threads,
position: 'top'
},
{
id: 'savedMessages',
label: chunter.string.SavedMessages,
icon: chunter.icon.Bookmark,
component: chunter.component.SavedMessages
}
],
spaces: [
{
label: chunter.string.Channels,
spaceClass: chunter.class.Channel,
addSpaceLabel: chunter.string.CreateChannel,
createComponent: chunter.component.CreateChannel
}
],
aside: chunter.component.ThreadView
}
},
chunter.app.Chunter
)
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.AttributePresenter, {
presenter: chunter.component.CommentPresenter
})
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
objectClass: chunter.class.Comment,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: chunter.activity.TxCommentCreate,
label: chunter.string.LeftComment,
display: 'content',
editable: true,
hideOnRemove: true
}, chunter.ids.TxCommentCreate)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: chunter.class.Comment,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: chunter.activity.TxCommentCreate,
label: chunter.string.LeftComment,
display: 'content',
editable: true,
hideOnRemove: true
},
chunter.ids.TxCommentCreate
)
// We need to define this one, to hide default attached object removed case
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
objectClass: chunter.class.Comment,
icon: chunter.icon.Chunter,
txClass: core.class.TxRemoveDoc,
display: 'inline',
hideOnRemove: true
}, chunter.ids.TxCommentRemove)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: chunter.class.Comment,
icon: chunter.icon.Chunter,
txClass: core.class.TxRemoveDoc,
display: 'inline',
hideOnRemove: true
},
chunter.ids.TxCommentRemove
)
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
objectClass: chunter.class.Backlink,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: chunter.activity.TxBacklinkCreate,
label: chunter.string.MentionedIn,
labelComponent: chunter.activity.TxBacklinkReference,
display: 'emphasized',
editable: false,
hideOnRemove: true
}, chunter.ids.TxCommentCreate)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: chunter.activity.TxBacklinkCreate,
label: chunter.string.MentionedIn,
labelComponent: chunter.activity.TxBacklinkReference,
display: 'emphasized',
editable: false,
hideOnRemove: true
},
chunter.ids.TxCommentCreate
)
// We need to define this one, to hide default attached object removed case
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
objectClass: chunter.class.Backlink,
icon: chunter.icon.Chunter,
txClass: core.class.TxRemoveDoc,
display: 'inline',
hideOnRemove: true
}, chunter.ids.TxBacklinkRemove)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
icon: chunter.icon.Chunter,
txClass: core.class.TxRemoveDoc,
display: 'inline',
hideOnRemove: true
},
chunter.ids.TxBacklinkRemove
)
}
export { chunterOperation } from './migration'

View File

@ -83,6 +83,20 @@ export function createModel (builder: Builder): void {
hidden: false,
navigatorModel: {
specials: [
{
id: 'spaceBrowser',
component: workbench.component.SpaceBrowser,
icon: workbench.icon.Search,
label: lead.string.FunnelBrowser,
position: 'top',
spaceClass: chunter.class.Channel,
componentProps: {
_class: lead.class.Funnel,
label: lead.string.FunnelBrowser,
createItemDialog: lead.component.CreateFunnel,
createItemLabel: lead.string.CreateFunnel
}
},
{
id: 'customers',
label: lead.string.Customers,

View File

@ -30,7 +30,8 @@ export default mergeIds(leadId, lead, {
Lead: '' as IntlString,
Title: '' as IntlString,
Assignee: '' as IntlString,
ManageFunnelStatuses: '' as IntlString
ManageFunnelStatuses: '' as IntlString,
FunnelBrowser: '' as IntlString
},
component: {
CreateFunnel: '' as AnyComponent,

View File

@ -21,11 +21,13 @@ import workbench, { workbenchId } from '@anticrm/workbench'
export default mergeIds(workbenchId, workbench, {
component: {
ApplicationPresenter: '' as AnyComponent,
Archive: '' as AnyComponent
Archive: '' as AnyComponent,
SpaceBrowser: '' as AnyComponent
},
string: {
Archive: '' as IntlString,
Application: '' as IntlString
Application: '' as IntlString,
SpaceBrowser: '' as IntlString
},
function: {
HasArchiveSpaces: '' as Resource<(spaces: Space[]) => boolean>

View File

@ -15,6 +15,11 @@
"Search": "Search...",
"Unassigned": "Unassigned",
"CreateMore": "Create more",
"NumberMembers": "{count, plural, =0 {no members} =1 {1 member} other {# members}}"
"NumberMembers": "{count, plural, =0 {no members} =1 {1 member} other {# members}}",
"InThis": "In this {space}",
"NoMatchesInThis": "No matches in this {space}",
"NoMatchesFound": "No matches found",
"NotInThis": "Not in this {space}",
"Add": "Add"
}
}

View File

@ -15,6 +15,11 @@
"Search": "Поиск...",
"Unassigned": "Не назначен",
"CreateMore": "Создать еще",
"NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участник} other {# участника}}"
"NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участник} other {# участника}}",
"InThis": "В этом {space}",
"NoMatchesInThis": "В этом {space} совпадения не обнаружены",
"NoMatchesFound": "Не найдено соответсвий",
"NotInThis": "Не в этом {space}",
"Add": "Добавить"
}
}

View File

@ -14,23 +14,99 @@
-->
<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 contact,{ Employee,EmployeeAccount } from '@anticrm/contact'
import { Account,DocumentQuery,Ref,SortingOrder,Space } from '@anticrm/core'
import { translate } from '@anticrm/platform'
import { Label,Scroller,SearchEdit } from '@anticrm/ui'
import presentation from '../plugin'
import { getClient } from '../utils'
import UserInfo from './UserInfo.svelte'
// const client = getClient()
export let space: Space
const client = getClient()
const hierarchy = client.getHierarchy()
$: label = hierarchy.getClass(space._class).label
let spaceClass = ''
$: { translate(label, {}).then((p) => spaceClass = p.toLowerCase()) }
let search: string = ''
$: isSearch = search.trim().length
let members: Set<Ref<Employee>> = new Set<Ref<Employee>>()
async function getUsers (accounts: Ref<Account>[], search: string): Promise<Employee[]> {
const query: DocumentQuery<EmployeeAccount> = isSearch > 0 ? { name: { $like: '%' + search + '%' } } : { _id: { $in: accounts as Ref<EmployeeAccount>[] } }
const employess = await client.findAll(contact.class.EmployeeAccount, query)
members = new Set(employess.filter((p) => accounts.includes(p._id)).map((p) => p.employee))
return await client.findAll(contact.class.Employee, {
_id: { $in: employess.map((e) => e.employee) }
}, { sort: { name: SortingOrder.Descending } })
}
async function add (employee: Ref<Employee>): Promise<void> {
const account = await client.findOne(contact.class.EmployeeAccount, { employee })
if (account === undefined) return
await client.update(space, {
$push: {
members: account._id
}
})
}
</script>
<div class="flex-col">
<div class="flex-row-center">
<span class="title"><Label label={presentation.string.Members} /></span>
</div>
<!-- TODO: implement Members -->
<div class="flex-col h-full">
<div class="ml-8 mr-8 mb-6 mt-4"><SearchEdit bind:value={search} /></div>
{#await getUsers(space.members, search) then users}
{@const current = users.filter((p) => members.has(p._id))}
{@const foreign = users.filter((p) => !members.has(p._id))}
{#if isSearch && !foreign.length && !current.length}
<div class="fs-title flex-center mt-10">
<Label label={presentation.string.NoMatchesFound} />
</div>
{:else}
<Scroller>
{#if isSearch}
<div class="pr-8 pl-8"><Label label={presentation.string.InThis} params={{ space: spaceClass }} /></div>
{#if !current.length}
<div class="fs-title pl-8 mb-4 mt-4">
<Label label={presentation.string.NoMatchesInThis} params={{ space: spaceClass }} />
</div>
{/if}
{/if}
{#each current as person}
<div class="item fs-title"><UserInfo size={'medium'} value={person} /></div>
{/each}
{#if foreign.length}
<div class="mt-4 notIn h-full">
<div class="divider w-full mb-4" />
<div class="pr-8 pl-8"><Label label={presentation.string.NotInThis} params={{ space: spaceClass }} /></div>
{#each foreign as person}
<div class="item flex-between">
<div class="fs-title"><UserInfo size={'medium'} value={person} /></div>
<div class="over-underline" on:click={() => add(person._id)}><Label label={presentation.string.Add} /></div>
</div>
{/each}
</div>
{/if}
</Scroller>
{/if}
{/await}
</div>
<style lang="scss">
.notIn {
background-color: var(--theme-bg-accent-color);
}
.divider {
background-color: var(--theme-dialog-divider);
height: 1px;
}
.item {
color: var(--caption-color);
cursor: pointer;
padding: 0.5rem 2rem;
&:hover, &:focus { background-color: var(--popup-bg-hover); }
}
</style>

View File

@ -44,7 +44,12 @@ export default plugin(presentationId, {
Search: '' as IntlString,
Unassigned: '' as IntlString,
CreateMore: '' as IntlString,
NumberMembers: '' as IntlString
NumberMembers: '' as IntlString,
InThis: '' as IntlString,
NoMatchesInThis: '' as IntlString,
NoMatchesFound: '' as IntlString,
NotInThis: '' as IntlString,
Add: '' as IntlString
},
metadata: {
RequiredVersion: '' as Metadata<string>

View File

@ -353,6 +353,7 @@ p:last-child { margin-block-end: 0; }
.pr-2 { padding-right: .5rem; }
.pr-3 { padding-right: .75rem; }
.pr-4 { padding-right: 1rem; }
.pr-8 { padding-right: 2rem; }
.pr-24 { padding-right: 6rem; }
.pt-2 { padding-top: .5rem; }
.pt-3 { padding-top: .75rem; }

View File

@ -49,6 +49,8 @@
"AddToSaved": "Add to saved",
"RemoveFromSaved": "Remove from saved",
"EmptySavedHeader": "Add messages to come back to later",
"EmptySavedText": "Tick off your to-dos or save something for another time. Only you can see your saved items, so use them however you like."
"EmptySavedText": "Tick off your to-dos or save something for another time. Only you can see your saved items, so use them however you like.",
"LeaveChannel": "Leave channel",
"ChannelBrowser": "Channel browser"
}
}

View File

@ -48,6 +48,8 @@
"AddToSaved": "Добавить в сохраненные",
"RemoveFromSaved": "Удалить из сохраненных",
"EmptySavedHeader": "Добавляйте сообщения и файлы, чтобы вернуться к ним позже",
"EmptySavedText": "Пометьте свои задачи или сохраните что-нибудь на потом. Только вы можете просматривать свои сохраненные объекты, поэтому используйте их как угодно."
"EmptySavedText": "Пометьте свои задачи или сохраните что-нибудь на потом. Только вы можете просматривать свои сохраненные объекты, поэтому используйте их как угодно.",
"LeaveChannel": "Покинуть канал",
"ChannelBrowser": "Браузер каналов"
}
}

View File

@ -28,7 +28,7 @@
export let _id: Ref<Channel>
export let _class: Ref<Class<Channel>>
let channel: Channel
let channel: Channel | undefined
const dispatch = createEventDispatcher()
@ -81,14 +81,15 @@
{/each}
<div class="ac-tabs__empty" />
</div>
<Scroller padding>
{#if channel}
{#if selectedTabIndex === 0}
<EditChannelDescriptionTab {channel} {_id} {_class} />
<Scroller padding>
<EditChannelDescriptionTab {channel} on:close />
</Scroller>
{:else if selectedTabIndex === 1}
<!-- Channel members -->
<Members />
<Members space={channel} />
{:else if selectedTabIndex === 2}
<EditChannelSettingsTab {channel} on:close />
{/if}
</Scroller>
{/if}
</div>

View File

@ -96,7 +96,7 @@
<style lang="scss">
.group {
border: 1px solid var(--theme-bg-focused-border);
border: 1px solid var(--theme-button-border-hovered);
border-radius: 12px;
padding: 16px 0;
}

View File

@ -15,34 +15,17 @@
-->
<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 { getCurrentAccount } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, EditBox } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import chunter from '../plugin'
import EditChannelDescriptionAttachments from './EditChannelDescriptionAttachments.svelte'
export let _id: Ref<Channel>
export let _class: Ref<Class<Channel>>
export let channel: Channel | undefined
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]
})
}
}
const dispatch = createEventDispatcher()
function onTopicChange (ev: Event) {
const newTopic = (ev.target as HTMLInputElement).value
@ -53,19 +36,17 @@
const newDescription = (ev.target as HTMLInputElement).value
client.update(channel!, { description: newDescription })
}
async function leaveChannel (): Promise<void> {
await client.update(channel, {
$pull: { members: getCurrentAccount()._id }
})
dispatch('close')
}
</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}
@ -82,6 +63,14 @@
focus
on:change={onDescriptionChange}
/>
<Button
label={chunter.string.LeaveChannel}
justify={'left'}
size={'x-large'}
on:click={() => {
leaveChannel()
}}
/>
<EditChannelDescriptionAttachments {channel} />
</div>
{/if}

View File

@ -11,10 +11,11 @@
</script>
{#if channel}
<div class="flex-col flex-gap-3">
<div class="mt-4 ml-8 mr-8 flex-col p-4">
<Button
label={chunter.string.ArchiveChannel}
justify={'left'}
size={'x-large'}
on:click={() => {
ArchiveChannel(channel, () => dispatch('close'))
}}

View File

@ -65,6 +65,8 @@ export default mergeIds(chunterId, chunter, {
AddToSaved: '' as IntlString,
RemoveFromSaved: '' as IntlString,
EmptySavedHeader: '' as IntlString,
EmptySavedText: '' as IntlString
EmptySavedText: '' as IntlString,
LeaveChannel: '' as IntlString,
ChannelBrowser: '' as IntlString
}
})

View File

@ -20,6 +20,7 @@
"Assignee": "Assignee",
"Title": "Title",
"LeadPlaceholder": "The simple lead",
"ManageFunnelStatuses": "Manage funnel statuses"
"ManageFunnelStatuses": "Manage funnel statuses",
"FunnelBrowser": "Funnel browser"
}
}

View File

@ -20,6 +20,7 @@
"Assignee": "Назначена",
"Title": "Загаловок",
"LeadPlaceholder": "Простая сделка",
"ManageFunnelStatuses": "Управление статусами воронки"
"ManageFunnelStatuses": "Управление статусами воронки",
"FunnelBrowser": "Браузер воронок"
}
}

View File

@ -19,7 +19,7 @@
import { Attachments } from '@anticrm/attachment-resources'
import type { Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { AttributesBar, createQuery, getClient } from '@anticrm/presentation'
import { AttributesBar, createQuery, getClient, Members } from '@anticrm/presentation'
import { Vacancy } from '@anticrm/recruit'
import { StyledTextBox } from '@anticrm/text-editor'
import { ActionIcon, Component, EditBox, Grid, Icon, IconClose, Label, Scroller, ToggleWithLabel } from '@anticrm/ui'
@ -104,9 +104,7 @@
</div>
</Scroller>
{:else if selected === 1}
<Scroller padding>
<ToggleWithLabel label={recruit.string.ThisVacancyIsPrivate} description={recruit.string.MakePrivateDescription}/>
</Scroller>
<Members space={object} />
{:else if selected === 2}
<Component is={activity.component.Activity} props={{ object, transparent: true }} />
{/if}

View File

@ -43,7 +43,7 @@ export { default as TableBrowser } from './components/TableBrowser.svelte'
export * from './context'
export * from './selection'
export { buildModel, getCollectionCounter, getObjectPresenter, LoadingProps } from './utils'
export { Table, TableView, EditDoc, ColorsPopup, Menu }
export { Table, TableView, EditDoc, ColorsPopup, Menu, SpacePresenter }
export default async (): Promise<Resources> => ({
actionImpl: actionImpl,

View File

@ -1,2 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="search" viewBox="0 0 16 16">
<path d="M14.4,13.6L11.7,11c0.8-1,1.3-2.2,1.3-3.5c0-3-2.5-5.5-5.5-5.5C4.5,2,2,4.5,2,7.5c0,3,2.5,5.5,5.5,5.5 c1.3,0,2.6-0.5,3.5-1.3l2.6,2.6c0.2,0.2,0.5,0.2,0.7,0C14.5,14.2,14.5,13.8,14.4,13.6z M3,7.5C3,5,5,3,7.5,3S12,5,12,7.5 S10,12,7.5,12S3,10,3,7.5z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 71 B

After

Width:  |  Height:  |  Size: 384 B

View File

@ -9,6 +9,11 @@
"Open": "Open",
"General": "General",
"Members": "Members",
"Application": "Application"
"Application": "Application",
"View": "View",
"Leave": "Leave",
"Joined": "Joined",
"Join": "Join",
"BrowseSpaces": "Browse spaces"
}
}

View File

@ -9,6 +9,11 @@
"Open": "Открыть",
"General": "Общее",
"Members": "Участники",
"Application": "Приложение"
"Application": "Приложение",
"View": "Посмотреть",
"Leave": "Покинуть",
"Joined": "Вы присоеденились",
"Join": "Присоедениться",
"BrowseSpaces": "Обзор пространств"
}
}

View File

@ -13,7 +13,12 @@
// limitations under the License.
//
import { addStringsLoader } from '@anticrm/platform'
import { workbenchId } from '@anticrm/workbench'
import { addStringsLoader, loadMetadata } from '@anticrm/platform'
import workbench, { workbenchId } from '@anticrm/workbench'
const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(workbench.icon, {
Search: `${icons}#search`
})
addStringsLoader(workbenchId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { Doc, Ref, SortingOrder, Space } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
@ -45,9 +44,14 @@
core.class.Space,
{
_class: { $in: getSpecialSpaceClass(model) }
// temp disabled, need way for default spaces
// members: getCurrentAccount()._id
},
(result) => { spaces = result },
{ sort: { name: SortingOrder.Ascending } })
(result) => {
spaces = result
},
{ sort: { name: SortingOrder.Ascending } }
)
}
let topSpecials: SpecialNavModel[] = []
@ -58,10 +62,14 @@
const preferenceQuery = createQuery()
preferenceQuery.query(preferece.class.SpacePreference, {}, (res) => {
preferences = new Map(res.map((r) => { return [r.attachedTo, r] }))
preferences = new Map(
res.map((r) => {
return [r.attachedTo, r]
})
)
})
async function update (model: NavigatorModel, spaces: Space[], preferences: Map<Ref<Doc>, SpacePreference>) {
async function update(model: NavigatorModel, spaces: Space[], preferences: Map<Ref<Doc>, SpacePreference>) {
if (model.specials !== undefined) {
topSpecials = await getSpecials(model.specials, 'top', spaces)
bottomSpecials = await getSpecials(model.specials, 'bottom', spaces)
@ -75,7 +83,11 @@
$: if (model) update(model, spaces, preferences)
async function getSpecials (specials: SpecialNavModel[], state: 'top' | 'bottom', spaces: Space[]): Promise<SpecialNavModel[]> {
async function getSpecials(
specials: SpecialNavModel[],
state: 'top' | 'bottom',
spaces: Space[]
): Promise<SpecialNavModel[]> {
const result: SpecialNavModel[] = []
for (const sp of specials) {
if ((sp.position ?? 'top') === state) {
@ -98,13 +110,25 @@
<Scroller>
{#if model.specials}
{#each topSpecials as special}
<SpecialElement label={special.label} icon={special.icon} on:click={() => dispatch('special', special.id)} selected={special.id === currentSpecial} indent={'ml-2'} />
<SpecialElement
label={special.label}
icon={special.icon}
on:click={() => dispatch('special', special.id)}
selected={special.id === currentSpecial}
indent={'ml-2'}
/>
{/each}
{#if topSpecials.length > 0 && bottomSpecials.length > 0}
<TreeSeparator />
{/if}
{#if topSpecials.length > 0 && bottomSpecials.length > 0}
<TreeSeparator />
{/if}
{#each bottomSpecials as special}
<SpecialElement label={special.label} icon={special.icon} on:click={() => dispatch('special', special.id)} selected={special.id === currentSpecial} indent={'ml-2'} />
<SpecialElement
label={special.label}
icon={special.icon}
on:click={() => dispatch('special', special.id)}
selected={special.id === currentSpecial}
indent={'ml-2'}
/>
{/each}
{/if}
@ -115,7 +139,14 @@
{/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}/>
<SpacesNav
spaces={shownSpaces.filter((it) => hierarchy.isDerived(it._class, m.spaceClass))}
{currentSpace}
hasSpaceBrowser={model.specials?.find((p) => p.id === 'spaceBrowser') !== undefined}
model={m}
on:space
{currentSpecial}
/>
{/each}
<div class="antiNav-space" />
</Scroller>

View File

@ -0,0 +1,156 @@
<!--
// 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,FindOptions,getCurrentAccount,Ref,SortingOrder,SortingQuery,Space } from '@anticrm/core'
import { AnyComponent, Button, getCurrentLocation, Icon, Label, navigate, Scroller, SearchEdit, showPopup } from '@anticrm/ui'
import presentation, { createQuery, getClient } from '@anticrm/presentation'
import plugin from '../plugin'
import { SpacePresenter } from '@anticrm/view-resources'
import { IntlString } from '@anticrm/platform'
import { classIcon } from '../utils'
export let _class: Ref<Class<Space>>
export let label: IntlString
export let createItemDialog: AnyComponent | undefined
export let createItemLabel: IntlString = presentation.string.Create
const me = getCurrentAccount()._id
const client = getClient()
const spaceQuery = createQuery()
let search: string = ''
let sort: SortingQuery<Space> = {
name: SortingOrder.Ascending
}
let spaces: Space[] = []
$: update(search, sort)
async function update (search: string, sort: SortingQuery<Space>): Promise<void> {
const query = search.trim().length > 0 ? { name: { $like: '%' + search + '%' } }: {}
const options: FindOptions<Space> = {
sort
}
spaceQuery.query(_class, query, (res) => {
spaces = res.filter((p) => !p.private || p.members.includes(me))
}, options)
}
function showCreateDialog (ev: Event) {
showPopup(createItemDialog as AnyComponent, { }, 'middle')
}
async function join (space: Space): Promise<void> {
if (space.members.includes(me)) return
await client.update(space, {
$push: {
members: me
}
})
}
async function leave (space: Space): Promise<void> {
if (!space.members.includes(me)) return
await client.update(space, {
$pull: {
members: me
}
})
}
async function view (space: Space): Promise<void> {
const loc = getCurrentLocation()
loc.path[2] = space._id
navigate(loc)
}
</script>
<div class="ac-header full divide">
<div class="ac-header__wrap-title">
<span class="ac-header__title"><Label {label} /></span>
</div>
{#if createItemDialog}
<Button label={createItemLabel} on:click={(ev) => showCreateDialog(ev)}/>
{/if}
</div>
<div class="ml-8 mr-8 mt-4"><SearchEdit bind:value={search} /></div>
<Scroller padding>
<div class="flex-col">
{#each spaces as space (space._id)}
{@const icon = classIcon(client, space._class)}
{@const joined = space.members.includes(me)}
<div class="divider"></div>
<div class="item flex-between">
<div>
<div class="fs-title flex">
{#if icon}
<Icon {icon} size={'small'} />
{/if}
<SpacePresenter value={space} />
</div>
<div>
{#if joined}
<Label label={plugin.string.Joined} />
&#183
{/if}
{space.members.length}
&#183
{space.description}
</div>
</div>
<div class="tools flex">
{#if joined}
<Button size={'x-large'} label={plugin.string.Leave} on:click={() => leave(space)}/>
{:else}
<div class="mr-2">
<Button size={'x-large'} label={plugin.string.View} on:click={() => view(space)}/>
</div>
<Button size={'x-large'} kind={'primary'} label={plugin.string.Join} on:click={() => join(space)}/>
{/if}
</div>
</div>
{/each}
<div class="flex-center mt-10">
<Button size={'x-large'} kind={'primary'} label={createItemLabel} on:click={(ev) => showCreateDialog(ev)}/>
</div>
</div>
</Scroller>
<style lang="scss">
.divider {
background-color: var(--theme-dialog-divider);
height: 1px;
}
.item {
color: var(--caption-color);
cursor: pointer;
padding: 1rem 0.75rem;
&:hover, &:focus {
background-color: var(--popup-bg-hover);
.tools {
visibility: visible;
}
}
.tools {
position: relative;
visibility: hidden;
}
}
</style>

View File

@ -13,12 +13,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Class, Ref, Space } from '@anticrm/core'
import core from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { createQuery, getClient, Members } from '@anticrm/presentation'
import { ActionIcon, EditBox, Grid, Icon, IconClose, Label, Scroller } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import workbench from '../../plugin'
@ -37,24 +36,32 @@
const clazz = client.getHierarchy().getClass(_class)
const query = createQuery()
$: query.query(core.class.Space, { _id }, result => { space = result[0] })
$: query.query(core.class.Space, { _id }, (result) => {
space = result[0]
})
const tabs: IntlString[] = [workbench.string.General, workbench.string.Members]
let selected = 0
function onNameChange (ev: Event) {
function onNameChange(ev: Event) {
const value = (ev.target as HTMLInputElement).value
if (value.trim().length > 0) {
client.updateDoc(_class, space.space, space._id, { name: value })
} else {
// Just refresh value
query.query(core.class.Space, { _id }, result => { space = result[0] })
query.query(core.class.Space, { _id }, (result) => {
space = result[0]
})
}
}
</script>
<div class="antiOverlay" on:click={() => { dispatch('close') }}/>
<div
class="antiOverlay"
on:click={() => {
dispatch('close')
}}
/>
<div class="antiDialogs antiComponent">
<div class="ac-header short mirror divide">
<div class="ac-header__wrap-title">
@ -63,12 +70,25 @@
{/if}
<div class="ac-header__title"><Label label={clazz.label} /></div>
</div>
<div class="tool"><ActionIcon icon={IconClose} size={'small'} action={() => { dispatch('close') }} /></div>
<div class="tool">
<ActionIcon
icon={IconClose}
size={'small'}
action={() => {
dispatch('close')
}}
/>
</div>
</div>
<div class="ac-tabs">
{#each tabs as tab, i}
<div class="ac-tabs__tab" class:selected={i === selected}
on:click={() => { selected = i }}>
<div
class="ac-tabs__tab"
class:selected={i === selected}
on:click={() => {
selected = i
}}
>
<Label label={tab} />
</div>
{/each}
@ -78,13 +98,21 @@
{#if selected === 0}
{#if space}
<Grid column={1} rowGap={1.5}>
<EditBox label={clazz.label} icon={clazz.icon} bind:value={space.name} placeholder={clazz.label} maxWidth="39rem" focus on:change={onNameChange}/>
<EditBox
label={clazz.label}
icon={clazz.icon}
bind:value={space.name}
placeholder={clazz.label}
maxWidth="39rem"
focus
on:change={onNameChange}
/>
<!-- <AttributeBarEditor maxWidth="39rem" object={space} key="name"/> -->
<!-- <ToggleWithLabel label={workbench.string.MakePrivate} description={workbench.string.MakePrivateDescription}/> -->
</Grid>
{/if}
{:else}
<Label label={workbench.string.Members} />
<Members {space} />
{/if}
</Scroller>
</div>

View File

@ -19,7 +19,17 @@
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { getResource } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import { Action, AnyComponent, IconAdd, IconEdit, showPanel, showPopup } from '@anticrm/ui'
import {
Action,
AnyComponent,
getCurrentLocation,
IconAdd,
IconEdit,
IconSearch,
navigate,
showPanel,
showPopup
} from '@anticrm/ui'
import view from '@anticrm/view'
import preference from '@anticrm/preference'
import { getActions as getContributedActions } from '@anticrm/view-resources'
@ -35,6 +45,7 @@
export let currentSpace: Ref<Space> | undefined
export let spaces: Space[]
export let currentSpecial: string | undefined
export let hasSpaceBrowser: boolean = false
const client = getClient()
const dispatch = createEventDispatcher()
@ -46,6 +57,16 @@
}
}
const browseSpaces: Action = {
label: plugin.string.BrowseSpaces,
icon: IconSearch,
action: async (_id: Ref<Doc>, ev?: Event): Promise<void> => {
const loc = getCurrentLocation()
loc.path[2] = 'spaceBrowser'
navigate(loc)
}
}
const editSpace: Action = {
label: plugin.string.Open,
icon: IconEdit,
@ -65,7 +86,7 @@
}
}
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
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)
@ -73,11 +94,11 @@
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[]> {
async function getActions(space: Space): Promise<Action[]> {
const result = [editSpace, starSpace]
const extraActions = await getContributedActions(client, space, core.class.Space)
@ -100,7 +121,7 @@
$: clazz = hierarchy.getClass(model.spaceClass)
$: lastEditMixin = hierarchy.as(clazz, notification.mixin.SpaceLastEdit)
function isChanged (space: Space, lastViews: Map<Ref<Doc>, number>): boolean {
function isChanged(space: Space, lastViews: Map<Ref<Doc>, number>): boolean {
const field = lastEditMixin?.lastEditField
const lastView = lastViews.get(space._id)
if (lastView === undefined || lastView === -1) return false
@ -110,9 +131,13 @@
return lastView < value
}
function getParentActions(): Action[] {
return hasSpaceBrowser ? [browseSpaces, addSpace] : [addSpace]
}
</script>
<TreeNode label={model.label} parent actions={async () => [addSpace]} indent={'ml-2'}>
<TreeNode label={model.label} parent actions={async () => getParentActions()} indent={'ml-2'}>
{#each spaces as space (space._id)}
{#if model.specials}
<TreeNode icon={model.icon} title={space.name} indent={'ml-2'} actions={() => getActions(space)}>

View File

@ -19,9 +19,10 @@ import { Resources } from '@anticrm/platform'
import Archive from './components/Archive.svelte'
import { Space } from '@anticrm/core'
import SpacePanel from './components/navigator/SpacePanel.svelte'
import SpaceBrowser from './components/SpaceBrowser.svelte'
function hasArchiveSpaces (spaces: Space[]): boolean {
return spaces.find(sp => sp.archived) !== undefined
function hasArchiveSpaces(spaces: Space[]): boolean {
return spaces.find((sp) => sp.archived) !== undefined
}
export default async (): Promise<Resources> => ({
@ -29,7 +30,8 @@ export default async (): Promise<Resources> => ({
WorkbenchApp,
ApplicationPresenter,
Archive,
SpacePanel
SpacePanel,
SpaceBrowser
},
function: {
HasArchiveSpaces: hasArchiveSpaces

View File

@ -29,7 +29,12 @@ export default mergeIds(workbenchId, workbench, {
Archived: '' as IntlString,
Open: '' as IntlString,
General: '' as IntlString,
Members: '' as IntlString
Members: '' as IntlString,
View: '' as IntlString,
Leave: '' as IntlString,
Joined: '' as IntlString,
Join: '' as IntlString,
BrowseSpaces: '' as IntlString
},
component: {
SpacePanel: '' as AnyComponent

View File

@ -107,6 +107,9 @@ export default plugin(workbenchId, {
component: {
WorkbenchApp: '' as AnyComponent
},
icon: {
Search: '' as Asset
},
metadata: {
PlatformTitle: '' as Metadata<string>,
ExcludedApplications: '' as Metadata<Ref<Application>[]>