Editable presenters (#2594)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-07 10:52:34 +06:00 committed by GitHub
parent 61fbb4b78b
commit 0bee06e8a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 167 additions and 36 deletions

View File

@ -713,7 +713,7 @@ export function createModel (builder: Builder): void {
view.filter.FilterNestedDontMatch
)
classPresenter(builder, core.class.EnumOf, view.component.StringPresenter, view.component.EnumEditor)
classPresenter(builder, core.class.EnumOf, view.component.EnumPresenter, view.component.EnumEditor)
createAction(builder, {
action: view.actionImpl.ShowPopup,

View File

@ -66,7 +66,8 @@ export default mergeIds(viewId, view, {
MarkupEditorPopup: '' as AnyComponent,
ListView: '' as AnyComponent,
IndexedDocumentPreview: '' as AnyComponent,
SpaceRefPresenter: '' as AnyComponent
SpaceRefPresenter: '' as AnyComponent,
EnumPresenter: '' as AnyComponent
},
string: {
Table: '' as IntlString,

View File

@ -24,7 +24,7 @@
import Label from './Label.svelte'
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let label: IntlString
export let label: IntlString | undefined = undefined
export let placeholder: IntlString | undefined = ui.string.SearchDots
export let items: DropdownTextItem[]
export let multiselect = false

View File

@ -1,5 +1,6 @@
<script lang="ts">
import type { Card } from '@hcengineering/board'
import { DateRangeMode } from '@hcengineering/core'
import { DatePresenter } from '@hcengineering/ui'
export let value: Card
@ -19,7 +20,7 @@
{#if value.dueDate}
<DatePresenter
bind:value={value.dueDate}
withTime={true}
mode={DateRangeMode.DATETIME}
icon={isOverdue ? 'overdue' : undefined}
{size}
kind="transparent"

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { AssigneeBox, createQuery } from '@hcengineering/presentation'
import { ButtonKind } from '@hcengineering/ui'
import { PersonLabelTooltip } from '..'
import contact from '../plugin'
@ -10,6 +10,7 @@
export let value: Ref<Employee> | null | undefined
export let kind: ButtonKind = 'link'
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
export let onChange: ((value: Ref<Employee>) => void) | undefined = undefined
let employee: Employee | undefined
const query = createQuery()
@ -26,14 +27,26 @@
}
</script>
<EmployeePresenter
value={getValue(employee, value)}
{tooltipLabels}
isInteractive={false}
shouldShowAvatar
shouldShowPlaceholder
defaultName={contact.string.NotSpecified}
shouldShowName={kind !== 'list'}
avatarSize={kind === 'list-header' ? 'small' : 'x-small'}
disableClick
/>
{#if onChange !== undefined}
<AssigneeBox
label={contact.string.Employee}
{value}
size={'medium'}
kind={'link'}
showNavigate={false}
justify={'left'}
on:change={({ detail }) => onChange?.(detail)}
/>
{:else}
<EmployeePresenter
value={getValue(employee, value)}
{tooltipLabels}
isInteractive={false}
shouldShowAvatar
shouldShowPlaceholder
defaultName={contact.string.NotSpecified}
shouldShowName={kind !== 'list'}
avatarSize={kind === 'list-header' ? 'small' : 'x-small'}
disableClick
/>
{/if}

View File

@ -20,10 +20,10 @@
import Won from '../icons/Won.svelte'
import Lost from '../icons/Lost.svelte'
export let value: DoneState
export let value: DoneState | null | undefined
export let showTitle: boolean = true
$: color = value._class === task.class.WonState ? getPlatformColor(0) : getPlatformColor(11)
$: color = value?._class === task.class.WonState ? getPlatformColor(0) : getPlatformColor(11)
</script>
{#if value}

View File

@ -20,12 +20,12 @@
import task from '@hcengineering/task'
import DoneStatePresenter from './DoneStatePresenter.svelte'
export let value: Ref<DoneState>
export let value: Ref<DoneState> | null | undefined
export let showTitle: boolean = true
let state: DoneState | undefined
const query = createQuery()
$: query.query(task.class.DoneState, { _id: value }, (res) => ([state] = res), { limit: 1 })
$: value && query.query(task.class.DoneState, { _id: value }, (res) => ([state] = res), { limit: 1 })
</script>
{#if state}

View File

@ -17,9 +17,11 @@
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { State } from '@hcengineering/task'
import StateEditor from './StateEditor.svelte'
import StatePresenter from './StatePresenter.svelte'
export let value: Ref<State>
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
let state: State | undefined
const query = createQuery()
@ -27,5 +29,9 @@
</script>
{#if state}
<StatePresenter value={state} />
{#if onChange !== undefined}
<StateEditor {value} space={state.space} {onChange} kind="link" size="medium" />
{:else}
<StatePresenter value={state} />
{/if}
{/if}

View File

@ -18,7 +18,12 @@
import { DateRangePresenter } from '@hcengineering/ui'
export let value: number | null | undefined
export let onChange: ((value: number | null) => void) | undefined = undefined
export let noShift: boolean = false
</script>
<DateRangePresenter {value} {noShift} />
{#if onChange !== undefined}
<DateRangePresenter {value} {noShift} editable on:change={(e) => onChange?.(e.detail)} />
{:else}
<DateRangePresenter {value} {noShift} />
{/if}

View File

@ -0,0 +1,60 @@
<!--
// 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, { EnumOf } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { DropdownLabels, DropdownTextItem } from '@hcengineering/ui'
import StringPresenter from './StringPresenter.svelte'
export let value: string
export let type: EnumOf | undefined = undefined
export let onChange: ((value: string) => void) | undefined = undefined
let items: DropdownTextItem[] = []
const query = createQuery()
$: type &&
query.query(
core.class.Enum,
{
_id: type.of
},
(res) => {
items = res[0]?.enumValues?.map((p) => {
return { id: p, label: p }
})
},
{ limit: 1 }
)
</script>
{#if onChange !== undefined && type !== undefined}
<DropdownLabels
bind:selected={value}
{items}
useFlexGrow={true}
justify={'left'}
size={'large'}
kind={'link'}
width={'100%'}
autoSelect={false}
on:selected={(e) => {
onChange?.(e.detail)
}}
/>
{:else}
<StringPresenter {value} />
{/if}

View File

@ -14,10 +14,10 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, FindOptions, Lookup, Ref } from '@hcengineering/core'
import core, { AnyAttribute, Class, Doc, DocumentQuery, FindOptions, Lookup, Ref } from '@hcengineering/core'
import { getObjectValue, SortingOrder } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, updateAttribute } from '@hcengineering/presentation'
import {
CheckBox,
Component,
@ -31,9 +31,9 @@
} from '@hcengineering/ui'
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import view from '../plugin'
import { buildConfigLookup, buildModel, LoadingProps } from '../utils'
import Menu from './Menu.svelte'
import view from '../plugin'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
@ -180,11 +180,14 @@
}
}
const joinProps = (collectionAttr: boolean, object: Doc, props: any) => {
if (collectionAttr) {
return { object, ...props }
const joinProps = (attribute: AttributeModel, object: Doc) => {
if (attribute.collectionAttr) {
return { object, ...attribute.props }
}
return props
if (attribute.attribute?.type._class === core.class.EnumOf) {
return { ...attribute.props, type: attribute.attribute.type }
}
return attribute.props
}
function getValue (attribute: AttributeModel, object: Doc): any {
if (attribute.castRequest) {
@ -195,6 +198,19 @@
}
return getObjectValue(attribute.key, object)
}
function onChange (value: any, doc: Doc, key: string, attribute: AnyAttribute) {
updateAttribute(client, doc, _class, { key, attr: attribute }, value)
}
function getOnChange (doc: Doc, attribute: AttributeModel) {
const attr = attribute.attribute
if (attr === undefined) return
if (attribute.collectionAttr) return
if (attribute.isLookup) return
const key = attribute.castRequest ? attribute.key.substring(attribute.castRequest.length + 1) : attribute.key
return (value: any) => onChange(value, doc, key, attr)
}
</script>
{#await buildModel({ client, _class, keys: config, lookup })}
@ -295,10 +311,12 @@
{#each model as attribute, cell}
<td>
<div class:antiTable-cells__firstCell={!cell}>
<!-- {getOnChange(object, attribute) !== undefined} -->
<svelte:component
this={attribute.presenter}
value={getValue(attribute, object)}
{...joinProps(attribute.collectionAttr, object, attribute.props)}
onChange={getOnChange(object, attribute)}
{...joinProps(attribute, object)}
/>
</div>
</td>

View File

@ -121,6 +121,7 @@
if (viewlet.hiddenKeys?.includes(attribute.name)) return
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) return
const value = getValue(attribute.name, attribute.type)
if (result.findIndex((p) => p.value === attribute.name) !== -1) return
if (result.findIndex((p) => p.value === value) !== -1) return
const { attrClass, category } = getAttributePresenterClass(hierarchy, attribute)
const typeClass = hierarchy.getClass(attrClass)

View File

@ -13,8 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { Doc, getObjectValue, Ref } from '@hcengineering/core'
import { AnyAttribute, Doc, getObjectValue, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { getClient, updateAttribute } from '@hcengineering/presentation'
import { CheckBox, Component, deviceOptionsStore as deviceInfo, tooltip } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
@ -50,6 +51,20 @@
$: elem && elementByIndex.set(index, elem)
$: indexById.set(docObject._id, index)
$: docByIndex.set(index, docObject)
const client = getClient()
function onChange (value: any, doc: Doc, key: string, attribute: AnyAttribute) {
updateAttribute(client, doc, doc._class, { key, attr: attribute }, value)
}
function getOnChange (docObject: Doc, attribute: AttributeModel) {
const attr = attribute.attribute
if (attr === undefined) return
if (attribute.collectionAttr) return
if (attribute.isLookup) return
return (value: any) => onChange(value, docObject, attribute.key, attr)
}
</script>
<div
@ -90,6 +105,7 @@
{...props}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
object={docObject}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
{...attributeModel.props}
/>
@ -100,6 +116,7 @@
{...props}
value={getObjectValue(attributeModel.key, docObject) ?? ''}
object={docObject}
onChange={getOnChange(docObject, attributeModel)}
kind={'list'}
{...attributeModel.props}
/>
@ -127,6 +144,7 @@
{...props}
value={value ?? ''}
objectId={docObject._id}
onChange={getOnChange(docObject, attributeModel)}
groupBy={groupByKey}
{...attributeModel.props}
/>

View File

@ -64,6 +64,7 @@ import ValueSelector from './components/ValueSelector.svelte'
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
import SpaceRefPresenter from './components/SpaceRefPresenter.svelte'
import EnumArrayEditor from './components/EnumArrayEditor.svelte'
import EnumPresenter from './components/EnumPresenter.svelte'
import {
afterResult,
@ -176,7 +177,8 @@ export default async (): Promise<Resources> => ({
GrowPresenter,
IndexedDocumentPreview,
SpaceRefPresenter,
EnumArrayEditor
EnumArrayEditor,
EnumPresenter
},
popup: {
PositionElementAlignment

View File

@ -92,7 +92,8 @@ export async function getObjectPresenter (
presenter,
props: preserveKey.props,
sortingKey,
collectionAttr: isCollectionAttr
collectionAttr: isCollectionAttr,
isLookup: false
}
}
@ -164,7 +165,8 @@ async function getAttributePresenter (
props: preserveKey.props,
icon: presenterMixin.icon,
attribute,
collectionAttr: isCollectionAttr
collectionAttr: isCollectionAttr,
isLookup: false
}
}
@ -185,7 +187,8 @@ export async function getPresenter<T extends Doc> (
label: label as IntlString,
presenter: typeof presenter === 'string' ? await getResource(presenter) : presenter,
props: preserveKey.props,
collectionAttr: isCollectionAttr
collectionAttr: isCollectionAttr,
isLookup: false
}
}
if (key.key.length === 0) {
@ -293,7 +296,8 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
label: stringKey as IntlString,
_class: core.class.TypeString,
props: { error: err },
collectionAttr: false
collectionAttr: false,
isLookup: false
}
return errorPresenter
}
@ -348,6 +352,7 @@ async function getLookupPresenter<T extends Doc> (
const lookupKey = { ...key, key: lookupProperty[0] }
const model = await getPresenter(client, lookupClass[0], lookupKey, preserveKey, undefined, lookupClass[2])
model.label = getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
model.isLookup = true
return model
}

View File

@ -416,6 +416,7 @@ export interface AttributeModel {
attribute?: AnyAttribute
collectionAttr: boolean
isLookup: boolean
castRequest?: Ref<Mixin<Doc>>
}