Custom attributes (#1650)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-05-05 20:50:28 +06:00 committed by GitHub
parent 342acb5bb8
commit 0be40e005e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 520 additions and 76 deletions

View File

@ -13,16 +13,11 @@
// limitations under the License. // limitations under the License.
// //
import type { Class, Ref, Type } from '@anticrm/core'
import core, { coreId } from '@anticrm/core' import core, { coreId } from '@anticrm/core'
import { IntlString, mergeIds } from '@anticrm/platform' import { IntlString, mergeIds } from '@anticrm/platform'
export default mergeIds(coreId, core, { export default mergeIds(coreId, core, {
class: {
Type: '' as Ref<Class<Type<any>>>
},
string: { string: {
Name: '' as IntlString,
Description: '' as IntlString, Description: '' as IntlString,
Private: '' as IntlString, Private: '' as IntlString,
Archived: '' as IntlString, Archived: '' as IntlString,

View File

@ -36,7 +36,7 @@ import {
Type, Type,
Version Version
} from '@anticrm/core' } from '@anticrm/core'
import { Index, Model, Prop, TypeIntlString, TypeRef, TypeString, TypeTimestamp } from '@anticrm/model' import { Index, Model, Prop, TypeIntlString, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import core from './component' import core from './component'
@ -106,46 +106,57 @@ export class TAttribute extends TDoc implements AnyAttribute {
name!: string name!: string
type!: Type<any> type!: Type<any>
label!: IntlString label!: IntlString
isCustom?: boolean
} }
@Model(core.class.Type, core.class.Obj) @Model(core.class.Type, core.class.Obj, DOMAIN_MODEL)
export class TType extends TObj implements Type<any> { export class TType extends TObj implements Type<any> {
label!: IntlString label!: IntlString
} }
@UX(core.string.String)
@Model(core.class.TypeString, core.class.Type) @Model(core.class.TypeString, core.class.Type)
export class TTypeString extends TType {} export class TTypeString extends TType {}
@UX(core.string.IntlString)
@Model(core.class.TypeIntlString, core.class.Type) @Model(core.class.TypeIntlString, core.class.Type)
export class TTypeIntlString extends TType {} export class TTypeIntlString extends TType {}
@UX(core.string.Number)
@Model(core.class.TypeNumber, core.class.Type) @Model(core.class.TypeNumber, core.class.Type)
export class TTypeNumber extends TType {} export class TTypeNumber extends TType {}
@UX(core.string.Markup)
@Model(core.class.TypeMarkup, core.class.Type) @Model(core.class.TypeMarkup, core.class.Type)
export class TTypeMarkup extends TType {} export class TTypeMarkup extends TType {}
@UX(core.string.Ref)
@Model(core.class.RefTo, core.class.Type) @Model(core.class.RefTo, core.class.Type)
export class TRefTo extends TType implements RefTo<Doc> { export class TRefTo extends TType implements RefTo<Doc> {
to!: Ref<Class<Doc>> to!: Ref<Class<Doc>>
} }
@UX(core.string.Collection)
@Model(core.class.Collection, core.class.Type) @Model(core.class.Collection, core.class.Type)
export class TCollection extends TType implements Collection<AttachedDoc> { export class TCollection extends TType implements Collection<AttachedDoc> {
of!: Ref<Class<Doc>> of!: Ref<Class<Doc>>
} }
@UX(core.string.Array)
@Model(core.class.ArrOf, core.class.Type) @Model(core.class.ArrOf, core.class.Type)
export class TArrOf extends TType implements ArrOf<Doc> { export class TArrOf extends TType implements ArrOf<Doc> {
of!: Type<Doc> of!: Type<Doc>
} }
@UX(core.string.Boolean)
@Model(core.class.TypeBoolean, core.class.Type) @Model(core.class.TypeBoolean, core.class.Type)
export class TTypeBoolean extends TType {} export class TTypeBoolean extends TType {}
@UX(core.string.Timestamp)
@Model(core.class.TypeTimestamp, core.class.Type) @Model(core.class.TypeTimestamp, core.class.Type)
export class TTypeTimestamp extends TType {} export class TTypeTimestamp extends TType {}
@UX(core.string.Date)
@Model(core.class.TypeDate, core.class.Type) @Model(core.class.TypeDate, core.class.Type)
export class TTypeDate extends TType {} export class TTypeDate extends TType {}

View File

@ -236,6 +236,22 @@ export function createModel (builder: Builder): void {
classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor) classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)
classPresenter(builder, core.class.Space, view.component.ObjectPresenter) classPresenter(builder, core.class.Space, view.component.ObjectPresenter)
builder.mixin(core.class.TypeString, core.class.Class, view.mixin.ObjectEditor, {
editor: view.component.StringTypeEditor
})
builder.mixin(core.class.TypeBoolean, core.class.Class, view.mixin.ObjectEditor, {
editor: view.component.BooleanTypeEditor
})
builder.mixin(core.class.TypeDate, core.class.Class, view.mixin.ObjectEditor, {
editor: view.component.DateTypeEditor
})
builder.mixin(core.class.TypeNumber, core.class.Class, view.mixin.ObjectEditor, {
editor: view.component.NumberTypeEditor
})
builder.createDoc( builder.createDoc(
view.class.ActionCategory, view.class.ActionCategory,
core.space.Model, core.space.Model,

View File

@ -73,7 +73,11 @@ export default mergeIds(viewId, view, {
TableView: '' as AnyComponent, TableView: '' as AnyComponent,
RolePresenter: '' as AnyComponent, RolePresenter: '' as AnyComponent,
YoutubePresenter: '' as AnyComponent, YoutubePresenter: '' as AnyComponent,
GithubPresenter: '' as AnyComponent GithubPresenter: '' as AnyComponent,
StringTypeEditor: '' as AnyComponent,
BooleanTypeEditor: '' as AnyComponent,
NumberTypeEditor: '' as AnyComponent,
DateTypeEditor: '' as AnyComponent
}, },
string: { string: {
Table: '' as IntlString, Table: '' as IntlString,

View File

@ -11,6 +11,17 @@
"Description": "Description", "Description": "Description",
"Private": "Private", "Private": "Private",
"Archived": "Archived", "Archived": "Archived",
"ClassLabel": "Label" "ClassLabel": "Label",
"String": "String",
"Markup": "Markup",
"Number": "Number",
"Boolean": "Boolean",
"Timestamp": "Timestamp",
"Date": "Date",
"IntlString": "IntlString",
"Ref": "Ref",
"Collection": "Collection",
"Array": "Array",
"Bag": "Bag"
} }
} }

View File

@ -11,6 +11,17 @@
"Description": "Описание", "Description": "Описание",
"Private": "Личный", "Private": "Личный",
"Archived": "Архивный", "Archived": "Архивный",
"ClassLabel": "Тип" "ClassLabel": "Тип",
"String": "Строка",
"Markup": "Разметка",
"Number": "Число",
"Boolean": "Логическое",
"Timestamp": "Времянная отметка",
"Date": "Дата",
"IntlString": "Интернационализированная строка",
"Ref": "Ссылка",
"Collection": "Коллекция",
"Array": "Массив",
"Bag": "Bag"
} }
} }

View File

@ -98,6 +98,7 @@ export interface Attribute<T extends PropertyType> extends Doc, UXObject {
name: string name: string
type: Type<T> type: Type<T>
index?: IndexKind index?: IndexKind
isCustom?: boolean
} }
/** /**

View File

@ -71,6 +71,7 @@ export default plugin(coreId, {
TxPutBag: '' as Ref<Class<TxPutBag<PropertyType>>>, TxPutBag: '' as Ref<Class<TxPutBag<PropertyType>>>,
Space: '' as Ref<Class<Space>>, Space: '' as Ref<Class<Space>>,
Account: '' as Ref<Class<Account>>, Account: '' as Ref<Class<Account>>,
Type: '' as Ref<Class<Type<any>>>,
TypeString: '' as Ref<Class<Type<string>>>, TypeString: '' as Ref<Class<Type<string>>>,
TypeIntlString: '' as Ref<Class<Type<IntlString>>>, TypeIntlString: '' as Ref<Class<Type<IntlString>>>,
TypeNumber: '' as Ref<Class<Type<string>>>, TypeNumber: '' as Ref<Class<Type<string>>>,
@ -109,6 +110,18 @@ export default plugin(coreId, {
ModifiedBy: '' as IntlString, ModifiedBy: '' as IntlString,
Class: '' as IntlString, Class: '' as IntlString,
AttachedTo: '' as IntlString, AttachedTo: '' as IntlString,
AttachedToClass: '' as IntlString AttachedToClass: '' as IntlString,
String: '' as IntlString,
Markup: '' as IntlString,
Number: '' as IntlString,
Boolean: '' as IntlString,
Timestamp: '' as IntlString,
Date: '' as IntlString,
IntlString: '' as IntlString,
Ref: '' as IntlString,
Collection: '' as IntlString,
Array: '' as IntlString,
Bag: '' as IntlString,
Name: '' as IntlString
} }
}) })

View File

@ -339,75 +339,75 @@ export class Builder {
* @public * @public
*/ */
export function TypeString (): Type<string> { export function TypeString (): Type<string> {
return { _class: core.class.TypeString, label: 'TypeString' as IntlString } return { _class: core.class.TypeString, label: core.string.String }
} }
/** /**
* @public * @public
*/ */
export function TypeNumber (): Type<number> { export function TypeNumber (): Type<number> {
return { _class: core.class.TypeNumber, label: 'TypeNumber' as IntlString } return { _class: core.class.TypeNumber, label: core.string.Number }
} }
/** /**
* @public * @public
*/ */
export function TypeMarkup (): Type<Markup> { export function TypeMarkup (): Type<Markup> {
return { _class: core.class.TypeMarkup, label: 'TypeMarkup' as IntlString } return { _class: core.class.TypeMarkup, label: core.string.Markup }
} }
/** /**
* @public * @public
*/ */
export function TypeIntlString (): Type<IntlString> { export function TypeIntlString (): Type<IntlString> {
return { _class: core.class.TypeIntlString, label: 'TypeIntlString' as IntlString } return { _class: core.class.TypeIntlString, label: core.string.IntlString }
} }
/** /**
* @public * @public
*/ */
export function TypeBoolean (): Type<boolean> { export function TypeBoolean (): Type<boolean> {
return { _class: core.class.TypeBoolean, label: 'TypeBoolean' as IntlString } return { _class: core.class.TypeBoolean, label: core.string.Boolean }
} }
/** /**
* @public * @public
*/ */
export function TypeTimestamp (): Type<Timestamp> { export function TypeTimestamp (): Type<Timestamp> {
return { _class: core.class.TypeTimestamp, label: 'TypeTimestamp' as IntlString } return { _class: core.class.TypeTimestamp, label: core.string.Timestamp }
} }
/** /**
* @public * @public
*/ */
export function TypeDate (withTime?: boolean): TypeDateType { export function TypeDate (withTime?: boolean): TypeDateType {
return { _class: core.class.TypeDate, label: 'TypeDate' as IntlString, withTime } return { _class: core.class.TypeDate, label: core.string.Date, withTime }
} }
/** /**
* @public * @public
*/ */
export function TypeRef (_class: Ref<Class<Doc>>): RefTo<Doc> { export function TypeRef (_class: Ref<Class<Doc>>): RefTo<Doc> {
return { _class: core.class.RefTo, to: _class, label: 'TypeRef' as IntlString } return { _class: core.class.RefTo, label: core.string.Ref, to: _class }
} }
/** /**
* @public * @public
*/ */
export function Collection<T extends AttachedDoc> (clazz: Ref<Class<T>>, itemLabel?: IntlString): TypeCollection<T> { export function Collection<T extends AttachedDoc> (clazz: Ref<Class<T>>, itemLabel?: IntlString): TypeCollection<T> {
return { _class: core.class.Collection, of: clazz, label: 'Collection' as IntlString, itemLabel } return { _class: core.class.Collection, label: core.string.Collection, of: clazz, itemLabel }
} }
/** /**
* @public * @public
*/ */
export function ArrOf<T extends PropertyType | Ref<Doc>> (type: Type<T>): TypeArrOf<T> { export function ArrOf<T extends PropertyType | Ref<Doc>> (type: Type<T>): TypeArrOf<T> {
return { _class: core.class.ArrOf, of: type, label: 'Array' as IntlString } return { _class: core.class.ArrOf, label: core.string.Array, of: type }
} }
/** /**
* @public * @public
*/ */
export function Bag (): Type<Record<string, PropertyType>> { export function Bag (): Type<Record<string, PropertyType>> {
return { _class: core.class.Bag, label: 'Bag' as IntlString } return { _class: core.class.Bag, label: core.string.Bag }
} }

View File

@ -16,7 +16,7 @@
import type { Plugin, IntlString } from './platform' import type { Plugin, IntlString } from './platform'
import { Status, Severity, unknownError } from './status' import { Status, Severity, unknownError } from './status'
import { _parseId } from './ident' import { _IdInfo, _parseId } from './ident'
import { setPlatformStatus } from './event' import { setPlatformStatus } from './event'
import { IntlMessageFormat } from 'intl-messageformat' import { IntlMessageFormat } from 'intl-messageformat'
@ -73,9 +73,8 @@ async function loadTranslationsForComponent (plugin: Plugin, locale: string): Pr
} }
} }
async function getTranslation (message: IntlString, locale: string): Promise<IntlString | Status> { async function getTranslation (id: _IdInfo, locale: string): Promise<IntlString | Status | undefined> {
try { try {
const id = _parseId(message)
let messages = translations.get(id.component) let messages = translations.get(id.component)
if (messages === undefined) { if (messages === undefined) {
messages = await loadTranslationsForComponent(id.component, locale) messages = await loadTranslationsForComponent(id.component, locale)
@ -84,11 +83,9 @@ async function getTranslation (message: IntlString, locale: string): Promise<Int
if (messages instanceof Status) { if (messages instanceof Status) {
return messages return messages
} }
return ( return id.kind !== undefined
(id.kind !== undefined ? (messages[id.kind] as Record<string, IntlString>)?.[id.name]
? (messages[id.kind] as Record<string, IntlString>)?.[id.name] : (messages[id.name] as IntlString)
: (messages[id.name] as IntlString)) ?? message
)
} catch (err) { } catch (err) {
const status = unknownError(err) const status = unknownError(err)
await setPlatformStatus(status) await setPlatformStatus(status)
@ -111,13 +108,24 @@ export async function translate<P extends Record<string, any>> (message: IntlStr
} }
return compiled.format(params) return compiled.format(params)
} else { } else {
const translation = await getTranslation(message, locale) try {
if (translation instanceof Status) { const id = _parseId(message)
cache.set(message, translation) if (id.component === 'embedded') {
return id.name
}
const translation = (await getTranslation(id, locale)) ?? message
if (translation instanceof Status) {
cache.set(message, translation)
return message
}
const compiled = new IntlMessageFormat(translation, locale)
cache.set(message, compiled)
return compiled.format(params)
} catch (err) {
const status = unknownError(err)
await setPlatformStatus(status)
cache.set(message, status)
return message return message
} }
const compiled = new IntlMessageFormat(translation, locale)
cache.set(message, compiled)
return compiled.format(params)
} }
} }

View File

@ -71,6 +71,11 @@ export type Namespace = Record<string, Record<string, string>>
*/ */
export const _ID_SEPARATOR = ':' export const _ID_SEPARATOR = ':'
/**
* @internal
*/
export const _EmbeddedId = 'embedded'
function identify (result: Record<string, any>, prefix: string, namespace: Record<string, any>): Namespace { function identify (result: Record<string, any>, prefix: string, namespace: Record<string, any>): Namespace {
for (const key in namespace) { for (const key in namespace) {
const value = namespace[key] const value = namespace[key]
@ -83,6 +88,13 @@ function identify (result: Record<string, any>, prefix: string, namespace: Recor
return result return result
} }
/**
* @public
*/
export function getEmbeddedLabel (str: string): IntlString {
return (_EmbeddedId + _ID_SEPARATOR + _EmbeddedId + _ID_SEPARATOR + str) as IntlString
}
/** /**
* Defines plugin Ids. * Defines plugin Ids.
* *

View File

@ -35,10 +35,10 @@
let container: HTMLElement let container: HTMLElement
let opened: boolean = false let opened: boolean = false
let selectedItem = items.find((x) => x.id === selected)
$: selectedItem = items.find((x) => x.id === selected) $: selectedItem = items.find((x) => x.id === selected)
$: if (selected === undefined && items[0] !== undefined) { $: if (selected === undefined && items[0] !== undefined) {
selected = items[0].id selected = items[0].id
dispatch('selected', selected)
} }
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()

View File

@ -37,14 +37,17 @@ async function createPseudoViewlet (
// Check if it is attached doc and collection have title override. // Check if it is attached doc and collection have title override.
const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' }) const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' })
if (presenter !== undefined) { if (presenter !== undefined) {
let collection = ''
if (dtx.collectionAttribute?.label !== undefined) {
collection = await translate(dtx.collectionAttribute.label, {})
}
return { return {
display, display,
icon: docClass.icon ?? activity.icon.Activity, icon: docClass.icon ?? activity.icon.Activity,
label: label, label: label,
labelParams: { labelParams: {
_class: trLabel, _class: trLabel,
collection: collection
dtx.collectionAttribute?.label !== undefined ? await translate(dtx.collectionAttribute?.label, {}) : ''
}, },
component: presenter.presenter, component: presenter.presenter,
pseudo: true pseudo: true

View File

@ -32,6 +32,10 @@
"General": "General", "General": "General",
"Navigation": "Navigation", "Navigation": "Navigation",
"Editor": "Editor", "Editor": "Editor",
"MarkdownFormatting": "Formatting" "MarkdownFormatting": "Formatting",
"Type": "Type",
"WithTime": "WithTime",
"CreatingAttribute": "Creating an attribute",
"CreatingAttributeConfirm": "Do you want to create an attribute? It will not be possible to change or delete it."
} }
} }

View File

@ -20,6 +20,10 @@
"General": "Общее", "General": "Общее",
"Navigation": "Навигация", "Navigation": "Навигация",
"Editor": "Редактор", "Editor": "Редактор",
"MarkdownFormatting": "Форматирование" "MarkdownFormatting": "Форматирование",
"Type": "Тип",
"WithTime": "Со временем",
"CreatingAttribute": "Создание атрибута",
"CreatingAttributeConfirm": "Вы хотите создать атрибут? Изменить или удалить его будет невозможно"
} }
} }

View File

@ -33,6 +33,7 @@
"svelte": "^3.47", "svelte": "^3.47",
"@anticrm/platform": "~0.6.6", "@anticrm/platform": "~0.6.6",
"@anticrm/contact": "~0.6.5", "@anticrm/contact": "~0.6.5",
"@anticrm/model": "~0.6.0",
"@anticrm/panel": "~0.6.0", "@anticrm/panel": "~0.6.0",
"@anticrm/core": "~0.6.16", "@anticrm/core": "~0.6.16",
"@anticrm/view": "~0.6.0", "@anticrm/view": "~0.6.0",

View File

@ -0,0 +1,61 @@
<!--
// 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 } from '@anticrm/core'
import presentation, { AttributesBar, getClient, KeyedAttribute } from '@anticrm/presentation'
import { ActionIcon, IconAdd, Label, showPopup } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import { collectionsFilter, getFiltredKeys } from '../utils'
export let object: Doc
export let objectClass: Class<Doc>
export let to: Ref<Class<Doc>> | undefined
export let ignoreKeys: string[] = []
export let vertical: boolean
const client = getClient()
const hierarchy = client.getHierarchy()
let keys: KeyedAttribute[] = []
function updateKeys (ignoreKeys: string[]): void {
const filtredKeys = getFiltredKeys(hierarchy, objectClass._id, ignoreKeys, to)
keys = collectionsFilter(hierarchy, filtredKeys, false)
}
$: updateKeys(ignoreKeys)
const dispatch = createEventDispatcher()
</script>
{#if vertical}
<div class="flex-between text-sm mb-4">
<Label label={objectClass.label} />
<ActionIcon
label={presentation.string.Create}
icon={IconAdd}
size="small"
action={() => {
showPopup(view.component.CreateAttribute, { _class: objectClass._id }, undefined, () => {
updateKeys(ignoreKeys)
dispatch('update')
})
}}
/>
</div>
{/if}
{#if keys.length || !vertical}
<AttributesBar {object} {keys} {vertical} />
{/if}

View File

@ -0,0 +1,118 @@
<!--
// 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, { Class, generateId, PropertyType, Ref, Space, Type } from '@anticrm/core'
import presentation, { getClient, MessageBox } from '@anticrm/presentation'
import { AnyComponent, EditBox, DropdownLabelsIntl, Label, Component, Button, showPopup } from '@anticrm/ui'
import { DropdownIntlItem } from '@anticrm/ui/src/types'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { getEmbeddedLabel } from '@anticrm/platform'
export let _class: Ref<Class<Space>>
let name: string
let type: Type<PropertyType> | undefined
let is: AnyComponent | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
async function save (): Promise<void> {
if (type === undefined) return
showPopup(
MessageBox,
{
label: view.string.CreatingAttribute,
message: view.string.CreatingAttributeConfirm
},
undefined,
async (result) => {
if (result && type !== undefined) {
await client.createDoc(core.class.Attribute, core.space.Model, {
attributeOf: _class,
name: name + generateId(),
label: getEmbeddedLabel(name),
isCustom: true,
type
})
dispatch('close')
}
}
)
}
function getTypes (): DropdownIntlItem[] {
const descendants = hierarchy.getDescendants(core.class.Type)
const res: DropdownIntlItem[] = []
for (const descendant of descendants) {
const _class = hierarchy.getClass(descendant)
if (_class.label !== undefined && hierarchy.hasMixin(_class, view.mixin.ObjectEditor)) {
res.push({
label: _class.label,
id: _class._id
})
}
}
return res
}
const items = getTypes()
let selectedType: Ref<Class<Type<PropertyType>>>
$: selectedType && selectType(selectedType)
function selectType (type: Ref<Class<Type<PropertyType>>>): void {
const _class = hierarchy.getClass(type)
const editor = hierarchy.as(_class, view.mixin.ObjectEditor)
if (editor.editor !== undefined) {
is = editor.editor
}
}
</script>
<div class="antiPopup w-60 p-4 flex-col">
<div class="mb-2"><EditBox bind:value={name} placeholder={core.string.Name} maxWidth="13rem" /></div>
<div class="flex-between mb-2">
<Label label={view.string.Type} />
<DropdownLabelsIntl
label={view.string.Type}
{items}
width="8rem"
bind:selected={selectedType}
on:selected={(e) => selectType(e.detail)}
/>
</div>
{#if is}
<Component
{is}
on:change={(e) => {
type = e.detail
}}
/>
{/if}
<div class="mt-2">
<Button
width="100%"
disabled={type === undefined || name === undefined || name.trim().length === 0}
label={presentation.string.Create}
on:click={() => {
save()
}}
/>
</div>
</div>
<style lang="scss">
</style>

View File

@ -0,0 +1,33 @@
<!--
// 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 { Doc, Mixin } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import ClassAttributeBar from './ClassAttributeBar.svelte'
export let object: Doc
export let mixins: Mixin<Doc>[]
export let ignoreKeys: string[]
const client = getClient()
const hierarchy = client.getHierarchy()
$: objectClass = hierarchy.getClass(object._class)
</script>
<ClassAttributeBar {objectClass} {object} {ignoreKeys} to={undefined} vertical on:update />
{#each mixins as mixin}
<div class="bottom-divider mt-4 mb-2" />
<ClassAttributeBar objectClass={mixin} {object} {ignoreKeys} to={objectClass._id} vertical on:update />
{/each}

View File

@ -15,7 +15,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact, { formatName } from '@anticrm/contact' import contact, { formatName } from '@anticrm/contact'
import core, { Class, ClassifierKind, Doc, Mixin, Obj, Ref } from '@anticrm/core' import { Class, ClassifierKind, Doc, Mixin, Obj, Ref } from '@anticrm/core'
import notification from '@anticrm/notification' import notification from '@anticrm/notification'
import { Panel } from '@anticrm/panel' import { Panel } from '@anticrm/panel'
import { Asset, getResource, translate } from '@anticrm/platform' import { Asset, getResource, translate } from '@anticrm/platform'
@ -29,8 +29,9 @@
import { AnyComponent, Component } from '@anticrm/ui' import { AnyComponent, Component } from '@anticrm/ui'
import view from '@anticrm/view' import view from '@anticrm/view'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { getCollectionCounter } from '../utils' import { collectionsFilter, getCollectionCounter, getFiltredKeys } from '../utils'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import DocAttributeBar from './DocAttributeBar.svelte'
import UpDownNavigator from './UpDownNavigator.svelte' import UpDownNavigator from './UpDownNavigator.svelte'
export let _id: Ref<Doc> export let _id: Ref<Doc>
@ -45,7 +46,6 @@
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res()) const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys())
$: read(_id) $: read(_id)
function read (_id: Ref<Doc>) { function read (_id: Ref<Doc>) {
@ -90,35 +90,21 @@
) )
} }
function filterKeys (keys: KeyedAttribute[], ignoreKeys: string[]): KeyedAttribute[] {
keys = keys.filter((k) => !docKeys.has(k.key))
keys = keys.filter((k) => !ignoreKeys.includes(k.key))
return keys
}
function getFiltredKeys (objectClass: Ref<Class<Doc>>, ignoreKeys: string[], to?: Ref<Class<Doc>>): KeyedAttribute[] {
const keys = [...hierarchy.getAllAttributes(objectClass, to).entries()]
.filter(([, value]) => value.hidden !== true)
.map(([key, attr]) => ({ key, attr }))
return filterKeys(keys, ignoreKeys)
}
let ignoreKeys: string[] = [] let ignoreKeys: string[] = []
let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>() let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
async function updateKeys (): Promise<void> { async function updateKeys (): Promise<void> {
const keysMap = new Map(getFiltredKeys(object._class, ignoreKeys).map((p) => [p.attr._id, p])) const keysMap = new Map(getFiltredKeys(hierarchy, object._class, ignoreKeys).map((p) => [p.attr._id, p]))
for (const m of mixins) { for (const m of mixins) {
const mkeys = getFiltredKeys(m._id, ignoreKeys) const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys)
for (const key of mkeys) { for (const key of mkeys) {
keysMap.set(key.attr._id, key) keysMap.set(key.attr._id, key)
} }
} }
const filtredKeys = Array.from(keysMap.values()) const filtredKeys = Array.from(keysMap.values())
keys = collectionsFilter(filtredKeys, false) keys = collectionsFilter(hierarchy, filtredKeys, false)
const collectionKeys = collectionsFilter(filtredKeys, true) const collectionKeys = collectionsFilter(hierarchy, filtredKeys, true)
const editors: { key: KeyedAttribute; editor: AnyComponent }[] = [] const editors: { key: KeyedAttribute; editor: AnyComponent }[] = []
for (const k of collectionKeys) { for (const k of collectionKeys) {
const editor = await getCollectionEditor(k) const editor = await getCollectionEditor(k)
@ -127,18 +113,6 @@
collectionEditors = editors collectionEditors = editors
} }
function collectionsFilter (keys: KeyedAttribute[], get: boolean): KeyedAttribute[] {
const result: KeyedAttribute[] = []
for (const key of keys) {
if (isCollectionAttr(key) === get) result.push(key)
}
return result
}
function isCollectionAttr (key: KeyedAttribute): boolean {
return hierarchy.isDerived(key.attr.type._class, core.class.Collection)
}
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent> { async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent> {
const clazz = hierarchy.getClass(_class) const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor) const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
@ -269,8 +243,10 @@
{#if !headerLoading} {#if !headerLoading}
{#if headerEditor !== undefined} {#if headerEditor !== undefined}
<Component is={headerEditor} props={{ object, keys, vertical: dir === 'column' }} /> <Component is={headerEditor} props={{ object, keys, vertical: dir === 'column' }} />
{:else if dir === 'column'}
<DocAttributeBar {object} {mixins} {ignoreKeys} on:update={updateKeys} />
{:else} {:else}
<AttributesBar {object} {keys} vertical={dir === 'column'} /> <AttributesBar {object} {keys} />
{/if} {/if}
{/if} {/if}
</svelte:fragment> </svelte:fragment>

View File

@ -0,0 +1,25 @@
<!--
// 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 { TypeBoolean } from '@anticrm/model'
import { onMount } from 'svelte'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
onMount(() => {
dispatch('change', TypeBoolean())
})
</script>

View File

@ -0,0 +1,40 @@
<!--
// 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 { TypeDate } from '@anticrm/model'
import { Label } from '@anticrm/ui'
import { onMount } from 'svelte'
import { createEventDispatcher } from 'svelte'
import view from '../../plugin'
import BooleanEditor from '../BooleanEditor.svelte'
const dispatch = createEventDispatcher()
let withTime: boolean = false
onMount(() => {
dispatch('change', TypeDate(withTime))
})
</script>
<div class="flex-between">
<Label label={view.string.WithTime} />
<BooleanEditor
bind:value={withTime}
onChange={(e) => {
dispatch('change', TypeDate(e))
}}
/>
</div>

View File

@ -0,0 +1,25 @@
<!--
// 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 { TypeNumber } from '@anticrm/model'
import { onMount } from 'svelte'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
onMount(() => {
dispatch('change', TypeNumber())
})
</script>

View File

@ -0,0 +1,25 @@
<!--
// 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 { TypeString } from '@anticrm/model'
import { onMount } from 'svelte'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
onMount(() => {
dispatch('change', TypeString())
})
</script>

View File

@ -38,6 +38,11 @@ import UpDownNavigator from './components/UpDownNavigator.svelte'
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte' import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte' import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
import ActionsPopup from './components/ActionsPopup.svelte' import ActionsPopup from './components/ActionsPopup.svelte'
import CreateAttribute from './components/CreateAttribute.svelte'
import StringTypeEditor from './components/typeEditors/StringTypeEditor.svelte'
import BooleanTypeEditor from './components/typeEditors/BooleanTypeEditor.svelte'
import DateTypeEditor from './components/typeEditors/DateTypeEditor.svelte'
import NumberTypeEditor from './components/typeEditors/NumberTypeEditor.svelte'
export { getActions } from './actions' export { getActions } from './actions'
export { default as ActionContext } from './components/ActionContext.svelte' export { default as ActionContext } from './components/ActionContext.svelte'
@ -53,6 +58,11 @@ export { Table, TableView, EditDoc, ColorsPopup, Menu, SpacePresenter, UpDownNav
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
actionImpl: actionImpl, actionImpl: actionImpl,
component: { component: {
CreateAttribute,
StringTypeEditor,
BooleanTypeEditor,
NumberTypeEditor,
DateTypeEditor,
SpacePresenter, SpacePresenter,
StringEditor, StringEditor,
StringPresenter, StringPresenter,

View File

@ -35,6 +35,10 @@ export default mergeIds(viewId, view, {
DeleteObjectConfirm: '' as IntlString, DeleteObjectConfirm: '' as IntlString,
Assignees: '' as IntlString, Assignees: '' as IntlString,
Labels: '' as IntlString, Labels: '' as IntlString,
ActionPlaceholder: '' as IntlString WithTime: '' as IntlString,
Type: '' as IntlString,
ActionPlaceholder: '' as IntlString,
CreatingAttribute: '' as IntlString,
CreatingAttributeConfirm: '' as IntlString
} }
}) })

View File

@ -283,3 +283,35 @@ export function getCollectionCounter (hierarchy: Hierarchy, object: Doc, key: Ke
} }
return (object as any)[key.key] ?? 0 return (object as any)[key.key] ?? 0
} }
function filterKeys (hierarchy: Hierarchy, keys: KeyedAttribute[], ignoreKeys: string[]): KeyedAttribute[] {
const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys())
keys = keys.filter((k) => !docKeys.has(k.key))
keys = keys.filter((k) => !ignoreKeys.includes(k.key))
return keys
}
export function getFiltredKeys (
hierarchy: Hierarchy,
objectClass: Ref<Class<Doc>>,
ignoreKeys: string[],
to?: Ref<Class<Doc>>
): KeyedAttribute[] {
const keys = [...hierarchy.getAllAttributes(objectClass, to).entries()]
.filter(([, value]) => value.hidden !== true)
.map(([key, attr]) => ({ key, attr }))
return filterKeys(hierarchy, keys, ignoreKeys)
}
export function collectionsFilter (hierarchy: Hierarchy, keys: KeyedAttribute[], get: boolean): KeyedAttribute[] {
const result: KeyedAttribute[] = []
for (const key of keys) {
if (isCollectionAttr(hierarchy, key) === get) result.push(key)
}
return result
}
function isCollectionAttr (hierarchy: Hierarchy, key: KeyedAttribute): boolean {
return hierarchy.isDerived(key.attr.type._class, core.class.Collection)
}

View File

@ -311,6 +311,7 @@ const view = plugin(viewId, {
component: { component: {
ObjectPresenter: '' as AnyComponent, ObjectPresenter: '' as AnyComponent,
EditDoc: '' as AnyComponent, EditDoc: '' as AnyComponent,
CreateAttribute: '' as AnyComponent,
SpacePresenter: '' as AnyComponent SpacePresenter: '' as AnyComponent
}, },
icon: { icon: {