New card form in Home (#9787)

* New card form

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Add tooltip for extend button

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2025-09-05 23:35:41 +07:00
committed by GitHub
parent 70805d2094
commit 8381d2ce70
21 changed files with 294 additions and 94 deletions
@@ -17,7 +17,7 @@
import { createEventDispatcher } from 'svelte'
import ui from '../plugin'
import { showPopup } from '../popups'
import type { ButtonKind, DropdownIntlItem } from '../types'
import type { ButtonKind, ButtonSize, DropdownIntlItem } from '../types'
import Button from './Button.svelte'
import DropdownIcon from './icons/Dropdown.svelte'
import NestedMenu from './NestedMenu.svelte'
@@ -30,6 +30,9 @@
export let width: string | undefined = undefined
export let kind: ButtonKind = 'regular'
export let size: ButtonSize | undefined = undefined
export let withIcon: boolean = false
export let withSelectIcon: boolean = true
let container: HTMLElement
let opened: boolean = false
@@ -39,7 +42,7 @@
function openPopup (): void {
if (!opened) {
opened = true
showPopup(NestedMenu, { items }, container, (result) => {
showPopup(NestedMenu, { items, withIcon }, container, (result) => {
if (result !== undefined) {
selected = result
dispatch('selected', result.id)
@@ -51,12 +54,22 @@
</script>
<div bind:this={container}>
<Button width={width ?? 'min-content'} {kind} {disabled} on:click={openPopup}>
<Button
width={width ?? 'min-content'}
{kind}
{size}
{disabled}
on:click={openPopup}
icon={withIcon ? selected?.icon : undefined}
iconProps={selected?.iconProps}
>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label label={selected !== undefined ? selected.label : label} />
</span>
<svelte:fragment slot="iconRight">
<DropdownIcon size={'small'} fill={!disabled ? 'var(--theme-content-color)' : 'var(--theme-dark-color)'} />
{#if withSelectIcon}
<DropdownIcon size={'small'} fill={!disabled ? 'var(--theme-content-color)' : 'var(--theme-dark-color)'} />
{/if}
</svelte:fragment>
</Button>
</div>
+16 -1
View File
@@ -18,10 +18,12 @@
import { resizeObserver } from '../resize'
import { DropdownIntlItem } from '../types'
import NestedMenu from './NestedMenu.svelte'
import Icon from './Icon.svelte'
export let items: [DropdownIntlItem, DropdownIntlItem[]][]
export let nestedFrom: DropdownIntlItem | undefined = undefined
export let onSelect: ((val: DropdownIntlItem) => void) | undefined = undefined
export let withIcon: boolean = false
const elements: HTMLElement[] = []
@@ -72,6 +74,11 @@
}
}}
>
{#if withIcon && nestedFrom.icon}
<div class="icon">
<Icon icon={nestedFrom.icon} iconProps={nestedFrom.iconProps} size={'small'} />
</div>
{/if}
<div class="overflow-label pr-1"><Label label={nestedFrom.label} /></div>
</button>
<div class="divider" />
@@ -87,12 +94,15 @@
elements[i]?.focus()
}}
label={item[0].label}
icon={withIcon ? item[0].icon : undefined}
iconProps={item[0].iconProps}
props={{
items: item[1].map((p) => {
return [p, []]
}),
nestedFrom: item[0],
onSelect: onNestedSelect
onSelect: onNestedSelect,
withIcon
}}
options={{ component: NestedMenu }}
/>
@@ -110,6 +120,11 @@
click(item[0])
}}
>
{#if withIcon && item[0].icon}
<div class="icon">
<Icon icon={item[0].icon} iconProps={item[0].iconProps} size={'small'} />
</div>
{/if}
<div class="overflow-label pr-1"><Label label={item[0].label} /></div>
</button>
{/if}
+2 -1
View File
@@ -27,6 +27,7 @@
export let focusIndex = -1
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let iconProps: Record<string, any> = {}
export let text: string | undefined = undefined
export let label: IntlString | undefined = undefined
export let labelProps: Record<string, any> = {}
@@ -58,7 +59,7 @@
{/if}
{:else}
{#if icon}
<div class="icon"><Icon {icon} size={'small'} /></div>
<div class="icon"><Icon {icon} {iconProps} size={'small'} /></div>
{/if}
<span class="overflow-label pr-1">
{#if label}<Label {label} params={labelProps} />
+4
View File
@@ -52,4 +52,8 @@
<symbol id="space" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9C2 6.23858 4.23858 4 7 4H10.6716C11.4672 4 12.2303 4.31607 12.7929 4.87868L15.6213 7.70711C15.8089 7.89464 16.0632 8 16.3284 8H25C27.7614 8 30 10.2386 30 13V23C30 25.7614 27.7614 28 25 28H7C4.23858 28 2 25.7614 2 23V9ZM7 6C5.34315 6 4 7.34315 4 9V23C4 24.6569 5.34315 26 7 26H25C26.6569 26 28 24.6569 28 23V13C28 11.3431 26.6569 10 25 10H16.3284C15.5328 10 14.7697 9.68393 14.2071 9.12132L11.3787 6.29289C11.1911 6.10536 10.9368 6 10.6716 6H7Z" fill="currentColor"/>
</symbol>
<symbol id="expand" viewBox="0 0 32 32">
<path d="M20 2V4H26.586L18.293 12.2929C17.9024 12.6834 17.9024 13.3166 18.293 13.7071C18.6835 14.0976 19.3167 14.0976 19.7072 13.7071L28 5.414V12H30V2H20Z"/>
<path d="M13.7073 19.7071C14.0978 19.3166 14.0978 18.6834 13.7073 18.2929C13.3167 17.9024 12.6836 17.9024 12.293 18.2929L4 26.586V20H2V30H12V28H5.414L13.7073 19.7071Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Rozšíření pro správu znalostí ve stylu databáze.",
"AddCollaborators": "Přidat spolupracovníky",
"Home": "Domů",
"WhatDoYouWantToShare": "Čím chcete sdílet?",
"Share": "Sdílet",
"CardTitle": "Zadejte název karty...",
"Post": "Přidat",
"Compact": "Kompaktní",
"Comfortable": "Komfortní",
"Comfortable2": "Komfortní 2"
"Comfortable2": "Komfortní 2",
"AdvancedCreateCard": "Přepnout na pokročilé vytváření karet"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Erweiterung für Datenbank-basiertes Wissensmanagement.",
"AddCollaborators": "Mitarbeiter hinzufügen",
"Home": "Startseite",
"WhatDoYouWantToShare": "Was möchten Sie teilen?",
"Share": "Teilen",
"CardTitle": "Geben Sie einen Kartentitel ein...",
"Post": "Beitrag",
"Compact": "Kompakt",
"Comfortable": "Komfortabel",
"Comfortable2": "Komfortabel 2"
"Comfortable2": "Komfortabel 2",
"AdvancedCreateCard": "Zum erweiterten Kartenerstellungsmodus wechseln"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extension for database-style knowledge management.",
"AddCollaborators": "Add collaborators",
"Home": "Home",
"WhatDoYouWantToShare": "What do you want to share?",
"Share": "Share",
"CardTitle": "Enter a card title...",
"Post": "Post",
"Compact": "Compact",
"Comfortable": "Comfortable",
"Comfortable2": "Comfortable 2"
"Comfortable2": "Comfortable 2",
"AdvancedCreateCard": "Switch to advanced card creation"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extensión para la gestión del conocimiento al estilo de una base de datos.",
"AddCollaborators": "Agregar colaboradores",
"Home": "Inicio",
"WhatDoYouWantToShare": "¿Qué quieres compartir?",
"Share": "Compartir",
"CardTitle": "Introduce un título para la tarjeta...",
"Post": "Publicar",
"Compact": "Compacto",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2"
"Comfortable2": "Confortable 2",
"AdvancedCreateCard": "Cambiar a creación avanzada de tarjetas"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extension pour la gestion des connaissances de style base de données.",
"AddCollaborators": "Ajouter des collaborateurs",
"Home": "Accueil",
"WhatDoYouWantToShare": "Que voulez-vous partager ?",
"Share": "Partager",
"CardTitle": "Entrez un titre de carte...",
"Post": "Publier",
"Compact": "Compact",
"Comfortable": "Confortable",
"Comfortable2": "Confortable 2"
"Comfortable2": "Confortable 2",
"AdvancedCreateCard": "Passer à la création avancée de cartes"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Estensione per la gestione della conoscenza in stile database.",
"AddCollaborators": "Aggiungi collaboratori",
"Home": "Home",
"WhatDoYouWantToShare": "Cosa vuoi condividere?",
"Share": "Condividi",
"CardTitle": "Inserisci un titolo per la carta...",
"Post": "Pubblica",
"Compact": "Compatto",
"Comfortable": "Confortabile",
"Comfortable2": "Confortabile 2"
"Comfortable2": "Confortabile 2",
"AdvancedCreateCard": "Passa alla creazione avanzata della carta"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "データベーススタイルのナレッジマネジメント用の拡張機能。",
"AddCollaborators": "コラボレーターを追加",
"Home": "ホーム",
"WhatDoYouWantToShare": "どうしますかを共有しますか?",
"Share": "共有",
"CardTitle": "カードのタイトルを入力...",
"Post": "投稿",
"Compact": "コンパクト",
"Comfortable": "コンファンティブ",
"Comfortable2": "コンファンティブ 2"
"Comfortable2": "コンファンティブ 2",
"AdvancedCreateCard": "高度なカード作成に切り替え"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Extensão para gerenciamento de conhecimento no estilo de banco de dados.",
"AddCollaborators": "Adicionar colaboradores",
"Home": "Início",
"WhatDoYouWantToShare": "O que você deseja compartilhar?",
"Share": "Compartilhar",
"CardTitle": "Digite um título para o cartão...",
"Post": "Publicar",
"Compact": "Compacto",
"Comfortable": "Confortável",
"Comfortable2": "Confortável 2"
"Comfortable2": "Confortável 2",
"AdvancedCreateCard": "Mudar para criação avançada de cartão"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "Расширение для управления знаниями в стиле базы данных.",
"AddCollaborators": "Добавить сотрудников",
"Home": "Дом",
"WhatDoYouWantToShare": "Чем вы хотите поделиться?",
"Share": "Поделиться",
"CardTitle": "Введите название карты...",
"Post": "Опубликовать",
"Compact": "Компактный",
"Comfortable": "Комфортный",
"Comfortable2": "Комфортный 2"
"Comfortable2": "Комфортный 2",
"AdvancedCreateCard": "Переключиться на расширенное создание карты"
}
}
+4 -3
View File
@@ -42,10 +42,11 @@
"ConfigDescription": "用于数据库风格知识管理的扩展。",
"AddCollaborators": "添加协作者",
"Home": "主页",
"WhatDoYouWantToShare": "您想分享什么?",
"Share": "分享",
"CardTitle": "输入卡片标题...",
"Post": "发布",
"Compact": "紧凑",
"Comfortable": "舒适",
"Comfortable2": "舒适 2"
"Comfortable2": "舒适 2",
"AdvancedCreateCard": "切换到高级卡片创建"
}
}
+2 -1
View File
@@ -25,5 +25,6 @@ loadMetadata(card.icon, {
View: `${icons}#view`,
Document: `${icons}#document`,
Home: `${icons}#home`,
Space: `${icons}#space`
Space: `${icons}#space`,
Expand: `${icons}#expand`
})
@@ -32,6 +32,7 @@
export let type: Ref<MasterTag> = 'chat:masterTag:Thread' as Ref<MasterTag>
export let space: CardSpace | undefined = undefined
export let changeType: boolean = false
export let allowChangeSpace: boolean = true
const dispatch = createEventDispatcher()
const client = getClient()
@@ -183,7 +184,7 @@
<TypeSelector bind:value={type} />
</div>
{/if}
{#if space == null && !(extension?.hideSpace ?? false)}
{#if (space == null || allowChangeSpace) && !(extension?.hideSpace ?? false)}
<div class="hulyModal-content__settingsSet-line">
<span class="label"><Label label={core.string.Space} /></span>
<SpaceSelector
@@ -11,7 +11,7 @@
<!-- See the License for the specific language governing permissions and -->
<!-- limitations under the License. -->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery } from '@hcengineering/presentation'
import { Card } from '@hcengineering/card'
import core, { SortingOrder } from '@hcengineering/core'
import ui, {
@@ -19,25 +19,21 @@
IconSettings,
Label,
ModernButton,
ModernEditbox,
Scroller,
SearchInput,
Loading,
showPopup
} from '@hcengineering/ui'
import { FilterBar, FilterButton } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import HomeCardPresenter from './HomeCardPresenter.svelte'
import HomeSettings from './HomeSettings.svelte'
import CreateCardPopup from './CreateCardPopup.svelte'
import NewCardForm from './NewCardForm.svelte'
import card from '../plugin'
const cardsQuery = createQuery()
const limitStep = 50
const dispatch = createEventDispatcher()
let divScroll: HTMLDivElement
let limit = limitStep
let cards: Card[] = []
@@ -111,19 +107,6 @@
function onSettings (e: MouseEvent): void {
showPopup(HomeSettings, {}, eventToHTMLElement(e))
}
function onShare (): void {
showPopup(CreateCardPopup, { title, changeType: true }, 'center', async (result) => {
if (result !== undefined) {
const doc = await getClient().findOne(card.class.Card, { _id: result })
if (doc === undefined) return
dispatch('selectCard', doc)
}
})
title = ''
}
let title: string = ''
</script>
<Scroller bind:divScroll {onScroll} padding="2rem 4rem">
@@ -145,28 +128,7 @@
space={undefined}
on:change={({ detail }) => (resultQuery = detail)}
/>
<div class="create-card">
<ModernEditbox
bind:value={title}
label={card.string.WhatDoYouWantToShare}
size="large"
kind="secondary"
width="100%"
disabled={false}
autoFocus={true}
>
<svelte:fragment slot="after">
<ModernButton
label={card.string.Share}
size="small"
kind="primary"
disabled={title.trim() === ''}
on:click={onShare}
/>
</svelte:fragment>
</ModernEditbox>
</div>
<NewCardForm />
<div class="body flex-gap-2">
{#each cards as card, index}
{@const previousCard = cards[index - 1]}
@@ -211,12 +173,6 @@
flex: 1;
}
.create-card {
display: flex;
width: 100%;
margin: 1rem 0;
}
.header {
display: flex;
width: 100%;
@@ -0,0 +1,178 @@
<script lang="ts">
import { getClient, SpaceSelector } from '@hcengineering/presentation'
import { ModernButton, ModernEditbox, ButtonIcon, IconSend, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import CreateCardPopup from './CreateCardPopup.svelte'
import card from '../plugin'
import { TypeSelector } from '../index'
import core, { Data, Ref } from '@hcengineering/core'
import { Card, type CardSpace, MasterTag } from '@hcengineering/card'
import { getResource } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { createCard } from '../utils'
const dispatch = createEventDispatcher()
let title: string = ''
let space: Ref<CardSpace> | undefined = undefined
let type: Ref<MasterTag> = 'chat:masterTag:Thread' as Ref<MasterTag>
let creating = false
$: applyDisabled = title.trim() === '' || space == null || type == null || creating
const client = getClient()
const hierarchy = client.getHierarchy()
$: extension =
type != null
? client
.getModel()
.findAllSync(card.mixin.CreateCardExtension, {})
.find((it) => hierarchy.isDerived(type, it._id))
: undefined
function onExtend (): void {
showPopup(
CreateCardPopup,
{ title, type, space, changeType: true, allowChangeSpace: true },
'center',
async (result) => {
if (result !== undefined) {
const doc = await getClient().findOne(card.class.Card, { _id: result })
if (doc === undefined) return
dispatch('selectCard', doc)
}
}
)
title = ''
}
async function okAction (): Promise<void> {
if (space === undefined || type == null || title.trim() === '') return
try {
creating = true
const data: Partial<Data<Card>> = {
title
}
if (extension?.canCreate !== undefined) {
const fn = await getResource(extension.canCreate)
const res = await fn(space, data)
if (res === false) {
return
} else if (typeof res === 'string') {
return
}
}
const createdCard = await createCard(type, space, data, EmptyMarkup)
if (createdCard != null) {
dispatch('selectCard', createdCard)
}
title = ''
} finally {
creating = false
}
}
</script>
<div class="create-card rounded-form">
<div class="corner-container">
<ButtonIcon
icon={card.icon.Expand}
size="extra-small"
on:click={onExtend}
kind="tertiary"
disabled={creating}
tooltip={{ label: card.string.AdvancedCreateCard }}
/>
</div>
<div class="form-row">
<ModernEditbox
bind:value={title}
label={card.string.CardTitle}
size="medium"
kind="ghost"
width="100%"
disabled={creating}
autoFocus={true}
on:keydown={(evt) => {
if (evt.key === 'Enter') {
void okAction()
}
}}
/>
</div>
<div class="form-row form-row-bottom">
<div class="form-dropdowns">
<SpaceSelector
_class={card.class.CardSpace}
query={{ archived: false }}
label={core.string.Space}
bind:space
focus={false}
readonly={creating}
kind={'regular'}
size={'small'}
/>
<TypeSelector size={'small'} bind:value={type} disabled={creating} />
</div>
<ModernButton
label={card.string.Post}
icon={IconSend}
iconSize="small"
size="small"
kind="primary"
disabled={applyDisabled}
loading={creating}
on:click={okAction}
/>
</div>
</div>
<style lang="scss">
.corner-container {
position: absolute;
top: 0.75rem;
right: 1rem;
z-index: 1;
display: flex;
align-items: center;
justify-content: flex-end;
pointer-events: auto;
}
.create-card {
display: flex;
flex-direction: column;
width: 100%;
margin: 1rem 0;
border-radius: 1rem;
border: 1px solid var(--theme-divider-color);
background: var(--theme-surface-color);
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
padding: 1rem;
position: relative;
}
.rounded-form {
border-radius: 1rem;
}
.form-row {
display: flex;
width: 100%;
margin-bottom: 1rem;
}
.form-row-bottom {
align-items: center;
justify-content: space-between;
margin-bottom: 0;
gap: 1rem;
}
.form-dropdowns {
display: flex;
gap: 0.75rem;
flex: 1;
}
</style>
@@ -15,13 +15,17 @@
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import { Class, ClassifierKind, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, NestedDropdown } from '@hcengineering/ui'
import { getClient, IconWithEmoji } from '@hcengineering/presentation'
import { type ButtonKind, type ButtonSize, DropdownIntlItem, NestedDropdown } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
import view from '@hcengineering/view'
export let value: Ref<MasterTag>
export let width: string | undefined = undefined
export let kind: ButtonKind | undefined = undefined
export let size: ButtonSize | undefined = undefined
export let disabled: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -55,21 +59,31 @@
try {
const clazz = hierarchy.getClass(key)
result.push([
{ id: key, label: clazz.label },
value.map((it) => ({ id: it._id, label: it.label })).sort((a, b) => a.label.localeCompare(b.label))
{ id: key, label: clazz.label, ...getIconProps(clazz) },
value
.map((it) => ({ id: it._id, label: it.label, ...getIconProps(it) }))
.sort((a, b) => a.label.localeCompare(b.label))
])
} catch {}
}
return result
}
function getIconProps (tag: MasterTag): Pick<DropdownIntlItem, 'icon' | 'iconProps'> {
return {
icon: tag.icon === view.ids.IconWithEmoji ? IconWithEmoji : tag.icon ?? card.icon.MasterTag,
iconProps: tag.icon === view.ids.IconWithEmoji ? { icon: tag.color } : {}
}
}
const classes = filterClasses()
const dispatch = createEventDispatcher()
$: selectedClass = hierarchy.getClass(value)
$: selected = {
id: selectedClass._id,
label: selectedClass.label
label: selectedClass.label,
...getIconProps(selectedClass)
}
</script>
@@ -77,6 +91,11 @@
items={classes}
{width}
{selected}
{kind}
{size}
{disabled}
withIcon={true}
withSelectIcon={false}
on:selected={(e) => {
value = e.detail
dispatch('change', value)
+4 -3
View File
@@ -102,10 +102,11 @@ export default mergeIds(cardId, card, {
NoChildren: '' as IntlString,
AddCollaborators: '' as IntlString,
Home: '' as IntlString,
WhatDoYouWantToShare: '' as IntlString,
Share: '' as IntlString,
CardTitle: '' as IntlString,
Post: '' as IntlString,
Compact: '' as IntlString,
Comfortable: '' as IntlString,
Comfortable2: '' as IntlString
Comfortable2: '' as IntlString,
AdvancedCreateCard: '' as IntlString
}
})
+2 -1
View File
@@ -151,7 +151,8 @@ const cardPlugin = plugin(cardId, {
View: '' as Asset,
Document: '' as Asset,
Home: '' as Asset,
Space: '' as Asset
Space: '' as Asset,
Expand: '' as Asset
},
extensions: {
EditCardExtension: '' as ComponentExtensionId