mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Card grid (#10749)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
"0.7.343"
|
||||
"0.7.410"
|
||||
|
||||
@@ -394,6 +394,15 @@ export function createSystemType (
|
||||
config: listConfig
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: type,
|
||||
descriptor: card.viewlet.CardGridDescriptor,
|
||||
baseQuery: {
|
||||
isLatest: true
|
||||
},
|
||||
config: []
|
||||
})
|
||||
|
||||
if (viewDefaults !== undefined) {
|
||||
builder.mixin(type, card.class.MasterTag, card.mixin.CardViewDefaults, viewDefaults)
|
||||
}
|
||||
@@ -781,6 +790,43 @@ export function createModel (builder: Builder): void {
|
||||
card.viewlet.CardRelationshipTable
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
label: card.string.Grid,
|
||||
icon: card.icon.Grid,
|
||||
component: card.component.CardGridView
|
||||
},
|
||||
card.viewlet.CardGridDescriptor
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: card.class.Card,
|
||||
descriptor: card.viewlet.CardGridDescriptor,
|
||||
baseQuery: {
|
||||
isLatest: true
|
||||
},
|
||||
config: [''],
|
||||
configOptions: {
|
||||
strict: true
|
||||
},
|
||||
viewOptions: {
|
||||
groupBy: [],
|
||||
orderBy: [
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['rank', SortingOrder.Ascending],
|
||||
['title', SortingOrder.Descending]
|
||||
],
|
||||
other: []
|
||||
}
|
||||
},
|
||||
card.viewlet.CardGrid
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
presentation.class.ComponentPointExtension,
|
||||
core.space.Model,
|
||||
|
||||
@@ -71,7 +71,7 @@ export const cardOperation: MigrateOperation = {
|
||||
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {
|
||||
await tryUpgrade(mode, state, client, cardId, [
|
||||
{
|
||||
state: 'migrateViewlets-v6',
|
||||
state: 'migrateViewlets-v7',
|
||||
func: migrateViewlets
|
||||
},
|
||||
{
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
class:content-default={!embedded}
|
||||
class:content-embedded={embedded}
|
||||
class:flex-center={fit && !audio}
|
||||
on:contextmenu
|
||||
style:min-height={fit ? '0' : `${minHeight ?? 0}px`}
|
||||
>
|
||||
{#await srcRef then src}
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
|
||||
<span class="emoji-{size} flex-row-center emoji" class:inline class:fitSize={inline}>
|
||||
{#if value !== undefined}
|
||||
{value}
|
||||
<span class="value">
|
||||
{value}
|
||||
</span>
|
||||
{:else}
|
||||
{#await getBlobRef(asRef(icon)) then iconBlob}
|
||||
<img src={iconBlob.src} srcset={iconBlob.srcset} alt="icon" />
|
||||
@@ -103,8 +105,19 @@
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.emoji-full {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
container-type: size;
|
||||
|
||||
.value {
|
||||
font-size: 70cqmin;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.emoji-smaller,
|
||||
.emoji-x-small,
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
|
||||
<div class="flex-presenter flex-gap-3">
|
||||
<DatePresenter value={value.createdOn} mode={DateRangeMode.DATETIME} kind="list" icon={IconScribble} />
|
||||
<PersonIdPresenter value={value.createdBy} shouldShowName={false} />
|
||||
<PersonIdPresenter value={value.createdBy} withPadding={false} shouldShowName={false} />
|
||||
</div>
|
||||
|
||||
@@ -76,4 +76,8 @@
|
||||
<symbol id="lock" viewBox="0 0 16 16">
|
||||
<path d="M4.69141 14.6338H11.3018C12.2178 14.6338 12.6689 14.1826 12.6689 13.1914V8.08496C12.6689 7.18945 12.293 6.72461 11.541 6.64941V4.96094C11.541 2.36328 9.81152 1.1123 7.99316 1.1123C6.18164 1.1123 4.45215 2.36328 4.45215 4.96094V6.67676C3.74805 6.78613 3.32422 7.2373 3.32422 8.08496V13.1914C3.32422 14.1826 3.77539 14.6338 4.69141 14.6338ZM5.75098 4.83105C5.75098 3.22461 6.76953 2.35645 7.99316 2.35645C9.2168 2.35645 10.2422 3.22461 10.2422 4.83105V6.64258L5.75098 6.64941V4.83105Z" />
|
||||
</symbol>
|
||||
<symbol id="grid" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M7.5 12.5H3.75L3.75 16.25H7.5V12.5ZM7.5 3.75H3.75L3.75 7.5H7.5V3.75ZM16.25 12.5H12.5V16.25H16.25V12.5ZM16.25 3.75H12.5V7.5H16.25V3.75ZM3.75 2.5C3.05964 2.5 2.5 3.05964 2.5 3.75V7.5C2.5 8.19036 3.05964 8.75 3.75 8.75H7.5C8.19036 8.75 8.75 8.19036 8.75 7.5V3.75C8.75 3.05964 8.19036 2.5 7.5 2.5H3.75ZM3.75 11.25C3.05964 11.25 2.5 11.8096 2.5 12.5V16.25C2.5 16.9404 3.05964 17.5 3.75 17.5H7.5C8.19036 17.5 8.75 16.9404 8.75 16.25V12.5C8.75 11.8096 8.19036 11.25 7.5 11.25H3.75ZM11.25 12.5C11.25 11.8096 11.8096 11.25 12.5 11.25H16.25C16.9404 11.25 17.5 11.8096 17.5 12.5V16.25C17.5 16.9404 16.9404 17.5 16.25 17.5H12.5C11.8096 17.5 11.25 16.9404 11.25 16.25V12.5ZM12.5 2.5C11.8096 2.5 11.25 3.05964 11.25 3.75V7.5C11.25 8.19036 11.8096 8.75 12.5 8.75H16.25C16.9404 8.75 17.5 8.19036 17.5 7.5V3.75C17.5 3.05964 16.9404 2.5 16.25 2.5H12.5Z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Sekce {section} odemknuta",
|
||||
"LayoutAuto": "Auto",
|
||||
"SingleColumn": "Jeden sloupec",
|
||||
"TwoColumns": "Dva sloupce"
|
||||
"TwoColumns": "Dva sloupce",
|
||||
"Grid": "Mřížka"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Sektion {section} entsperrt",
|
||||
"LayoutAuto": "Auto",
|
||||
"SingleColumn": "Einspaltig",
|
||||
"TwoColumns": "Zwei spaltig"
|
||||
"TwoColumns": "Zwei spaltig",
|
||||
"Grid": "Gitter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Section {section} unlocked",
|
||||
"LayoutAuto": "Auto",
|
||||
"SingleColumn": "Single column",
|
||||
"TwoColumns": "Two columns"
|
||||
"TwoColumns": "Two columns",
|
||||
"Grid": "Grid"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Sección {section} desbloqueada",
|
||||
"LayoutAuto": "Auto",
|
||||
"SingleColumn": "Una columna",
|
||||
"TwoColumns": "Dos columnas"
|
||||
"TwoColumns": "Dos columnas",
|
||||
"Grid": "Cuadrícula"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Section {section} déverrouillée",
|
||||
"LayoutAuto": "Auto",
|
||||
"SingleColumn": "Une colonne",
|
||||
"TwoColumns": "Deux colonnes"
|
||||
"TwoColumns": "Deux colonnes",
|
||||
"Grid": "Grille"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Sezione {section} sbloccata",
|
||||
"LayoutAuto": "Auto",
|
||||
"SingleColumn": "Una colonna",
|
||||
"TwoColumns": "Due colonne"
|
||||
"TwoColumns": "Due colonne",
|
||||
"Grid": "Griglia"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "セクション {section} アンロック済み",
|
||||
"LayoutAuto": "自動",
|
||||
"SingleColumn": "1列",
|
||||
"TwoColumns": "2列"
|
||||
"TwoColumns": "2列",
|
||||
"Grid": "グリッド"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Seção {section} desbloqueada",
|
||||
"LayoutAuto": "Automático",
|
||||
"SingleColumn": "Uma coluna",
|
||||
"TwoColumns": "Duas colunas"
|
||||
"TwoColumns": "Duas colunas",
|
||||
"Grid": "Grade"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Seção {section} desbloqueada",
|
||||
"LayoutAuto": "Automático",
|
||||
"SingleColumn": "Uma coluna",
|
||||
"TwoColumns": "Duas colunas"
|
||||
"TwoColumns": "Duas colunas",
|
||||
"Grid": "Grade"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Секция {section} разблокирована",
|
||||
"LayoutAuto": "Авто",
|
||||
"SingleColumn": "Один столбец",
|
||||
"TwoColumns": "Два столбца"
|
||||
"TwoColumns": "Два столбца",
|
||||
"Grid": "Сетка"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "Bölüm {section} kilidi açıldı",
|
||||
"LayoutAuto": "Otomatik",
|
||||
"SingleColumn": "Tek sütun",
|
||||
"TwoColumns": "İki sütun"
|
||||
"TwoColumns": "İki sütun",
|
||||
"Grid": "Izgara"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"SectionUnlocked": "部分 {section} 已解锁",
|
||||
"LayoutAuto": "自动",
|
||||
"SingleColumn": "单列",
|
||||
"TwoColumns": "两列"
|
||||
"TwoColumns": "两列",
|
||||
"Grid": "网格"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,6 @@ loadMetadata(card.icon, {
|
||||
Feed: `${icons}#feed`,
|
||||
All: `${icons}#all`,
|
||||
Duplicate: `${icons}#duplicate`,
|
||||
Lock: `${icons}#lock`
|
||||
Lock: `${icons}#lock`,
|
||||
Grid: `${icons}#grid`
|
||||
})
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Card } from '@hcengineering/card'
|
||||
import { type WithLookup } from '@hcengineering/core'
|
||||
import { getClient, Image, remToPx } from '@hcengineering/presentation'
|
||||
import { Button, IconMoreH } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { openDoc, PersonIdPresenter, showMenu, TimestampPresenter } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import card from '../plugin'
|
||||
import CardIcon from './CardIcon.svelte'
|
||||
import CardPresenter from './CardPresenter.svelte'
|
||||
|
||||
export let object: WithLookup<Card>
|
||||
export let selected: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const dispatch = createEventDispatcher()
|
||||
const size = remToPx(20)
|
||||
|
||||
let hovered = false
|
||||
|
||||
$: isFile = hierarchy.isDerived(object._class, card.types.File)
|
||||
$: mainBlob = Object.values(object.blobs ?? {})[0]
|
||||
$: isImage = mainBlob?.type?.startsWith('image/')
|
||||
$: previewRef = mainBlob?.file
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="card-container"
|
||||
class:selected
|
||||
class:hovered
|
||||
draggable="false"
|
||||
on:mouseover={() => dispatch('obj-focus', object)}
|
||||
on:mouseenter={() => dispatch('obj-focus', object)}
|
||||
on:focus={() => {}}
|
||||
on:contextmenu={(evt) => {
|
||||
showMenu(evt, { object })
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="card"
|
||||
on:click={() => {
|
||||
void openDoc(hierarchy, object)
|
||||
}}
|
||||
>
|
||||
<div class="card-content" class:is-file={isFile}>
|
||||
{#if isFile && previewRef && isImage}
|
||||
<Image
|
||||
blob={previewRef}
|
||||
alt={object.title}
|
||||
width={size}
|
||||
height={size}
|
||||
blurhash={mainBlob?.metadata?.thumbnail?.blurhash}
|
||||
responsive
|
||||
fit={'cover'}
|
||||
/>
|
||||
{:else if isFile && mainBlob}
|
||||
<div class="flex-center ext-icon">
|
||||
{mainBlob.name.split('.').pop()?.substring(0, 4).toUpperCase()}
|
||||
</div>
|
||||
{:else}
|
||||
<CardIcon value={object} size={'full'} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="header flex-col">
|
||||
<div class="flex-row-center flex-gap-2 h-8">
|
||||
<div class="title overflow-label flex-grow font-medium-14">
|
||||
<CardPresenter value={object} type={'text'} />
|
||||
</div>
|
||||
<div class="tools">
|
||||
<Button
|
||||
icon={IconMoreH}
|
||||
kind="ghost"
|
||||
size="medium"
|
||||
showTooltip={{ label: view.string.MoreActions, direction: 'bottom' }}
|
||||
on:click={(evt) => {
|
||||
hovered = true
|
||||
showMenu(evt, { object }, () => {
|
||||
hovered = false
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-between flex-gap-2 h-4 mt-2">
|
||||
<PersonIdPresenter value={object.createdBy} withPadding={false} noUnderline avatarSize="tiny" />
|
||||
<span class="flex-no-shrink caption-color">
|
||||
<TimestampPresenter value={object.createdOn ?? object.modifiedOn} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
position: relative;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
background-color: var(--theme-kanban-card-bg-color);
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease-in-out;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.05),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.08),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.04);
|
||||
border-color: var(--theme-border-color-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
outline: 2px solid var(--global-focus-BorderColor);
|
||||
outline-offset: 2px;
|
||||
background-color: var(--highlight-hover);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.hovered {
|
||||
.tools {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tools {
|
||||
display: none;
|
||||
background: var(--theme-kanban-card-bg-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 11rem;
|
||||
background: linear-gradient(180deg, var(--theme-border-color-light) 0%, var(--theme-divider-color) 100%);
|
||||
position: relative;
|
||||
|
||||
&.is-file {
|
||||
background: var(--theme-bg-color-alt);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
padding: 0.25rem 0.75rem 0.75rem 0.75rem;
|
||||
background-color: var(--theme-kanban-card-bg-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--theme-text-color);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.ext-icon {
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-default);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { type Card } from '@hcengineering/card'
|
||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { BuildModelKey, ViewOptions } from '@hcengineering/view'
|
||||
import { ListSelectionProvider, SelectDirection, buildConfigLookup, focusStore } from '@hcengineering/view-resources'
|
||||
import CardGridItem from './CardGridItem.svelte'
|
||||
|
||||
export let _class: Ref<Class<Card>>
|
||||
export let query: DocumentQuery<Card>
|
||||
export let config: Array<BuildModelKey | string>
|
||||
export let options: FindOptions<Card> | undefined = undefined
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const q = createQuery()
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
if (dir === 'vertical') {
|
||||
let pos = objects.findIndex((p) => p._id === of?._id)
|
||||
pos += offset
|
||||
if (pos < 0) {
|
||||
pos = 0
|
||||
}
|
||||
if (pos >= objects.length) {
|
||||
pos = objects.length - 1
|
||||
}
|
||||
listProvider.updateFocus(objects[pos])
|
||||
}
|
||||
})
|
||||
|
||||
let objects: WithLookup<Card>[] = []
|
||||
|
||||
$: orderBy = viewOptions.orderBy
|
||||
$: lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
|
||||
|
||||
$: q.query(
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
objects = result
|
||||
},
|
||||
{
|
||||
...options,
|
||||
sort: {
|
||||
...(options != null ? options.sort : {}),
|
||||
...(orderBy != null ? { [orderBy[0]]: orderBy[1] } : {})
|
||||
},
|
||||
lookup
|
||||
}
|
||||
)
|
||||
|
||||
$: listProvider.update(objects)
|
||||
$: selection = listProvider.current($focusStore)
|
||||
</script>
|
||||
|
||||
<ActionContext context={{ mode: 'browser' }} />
|
||||
|
||||
<div class="grid-container">
|
||||
{#each objects as object, i}
|
||||
{@const selected = selection === i}
|
||||
<div class="grid-cell">
|
||||
<CardGridItem
|
||||
{object}
|
||||
{selected}
|
||||
on:obj-focus={(evt) => {
|
||||
listProvider.updateFocus(evt.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-container {
|
||||
display: grid;
|
||||
margin: 0.5rem 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,15 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import card, { Card } from '@hcengineering/card'
|
||||
import { getClient, FilePreview } from '@hcengineering/presentation'
|
||||
import { BlobType } from '@hcengineering/core'
|
||||
import presentation, {
|
||||
deleteFile,
|
||||
FilePreview,
|
||||
getClient,
|
||||
getFileUrl,
|
||||
IconDownload
|
||||
} from '@hcengineering/presentation'
|
||||
import { getEventPositionElement, IconDelete, Menu, showPopup, type Action } from '@hcengineering/ui'
|
||||
import Description from './Description.svelte'
|
||||
import FilePlaceholder from './FilePlaceholder.svelte'
|
||||
|
||||
@@ -27,6 +35,44 @@
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: isFile = hierarchy.isDerived(doc._class, card.types.File)
|
||||
|
||||
async function showContextMenu (blob: BlobType, ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const actions: Action[] = [
|
||||
{
|
||||
label: presentation.string.Download,
|
||||
icon: IconDownload,
|
||||
action: async () => {
|
||||
const url = getFileUrl(blob.file, blob.name)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = blob.name
|
||||
a.target = '_blank'
|
||||
a.click()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (!readonly) {
|
||||
actions.push({
|
||||
label: presentation.string.Delete,
|
||||
icon: IconDelete,
|
||||
action: async () => {
|
||||
const blobs = { ...doc.blobs }
|
||||
const key = Object.keys(blobs).find((k) => blobs[k] === blob)
|
||||
if (key) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete blobs[key]
|
||||
await client.update(doc, { blobs })
|
||||
await deleteFile(blob.file)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showPopup(Menu, { actions }, getEventPositionElement(ev))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isFile && Object.keys(doc.blobs ?? {}).length === 0 && !readonly}
|
||||
@@ -40,6 +86,7 @@
|
||||
name={blob.name}
|
||||
metadata={blob.metadata}
|
||||
fit={blob.type !== 'application/pdf'}
|
||||
on:contextmenu={(ev) => showContextMenu(blob, ev)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
view.viewlet.List,
|
||||
view.viewlet.Tree,
|
||||
view.viewlet.MasterDetail,
|
||||
card.viewlet.CardGrid,
|
||||
card.viewlet.CardFeedDescriptor,
|
||||
view.viewlet.RelationshipTable,
|
||||
view.viewlet.Document
|
||||
@@ -37,6 +38,7 @@
|
||||
view.viewlet.List,
|
||||
view.viewlet.Tree,
|
||||
view.viewlet.MasterDetail,
|
||||
card.viewlet.CardGrid,
|
||||
card.viewlet.CardFeedDescriptor,
|
||||
view.viewlet.RelationshipTable
|
||||
]
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
createChildAction
|
||||
} from './utils'
|
||||
import { formatCardValue } from './cardTableFormatter'
|
||||
import CardGridView from './components/CardGridView.svelte'
|
||||
import ManageMasterTagsContent from './components/settings/ManageMasterTagsContent.svelte'
|
||||
import ManageMasterTagsTools from './components/settings/ManageMasterTagsTools.svelte'
|
||||
import ManageMasterTags from './components/settings/ManageMasterTags.svelte'
|
||||
@@ -135,6 +136,7 @@ export default async (): Promise<Resources> => ({
|
||||
CardTagsColored,
|
||||
CardIcon,
|
||||
CardFeedView,
|
||||
CardGridView,
|
||||
CreateSpace,
|
||||
CardHeaderButton,
|
||||
CreateRolePopup,
|
||||
|
||||
@@ -56,7 +56,8 @@ export default mergeIds(cardId, card, {
|
||||
CreateCard: '' as AnyComponent,
|
||||
CardHeaderButton: '' as AnyComponent,
|
||||
CreateRolePopup: '' as AnyComponent,
|
||||
MyCards: '' as AnyComponent
|
||||
MyCards: '' as AnyComponent,
|
||||
CardGridView: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
CardFactory: '' as Resource<(props?: Record<string, any>) => Promise<Ref<Doc> | undefined>>,
|
||||
@@ -96,6 +97,8 @@ export default mergeIds(cardId, card, {
|
||||
CardChildList: '' as Ref<Viewlet>,
|
||||
CardFeedDescriptor: '' as Ref<ViewletDescriptor>,
|
||||
CardFeed: '' as Ref<Viewlet>,
|
||||
CardGridDescriptor: '' as Ref<ViewletDescriptor>,
|
||||
CardGrid: '' as Ref<Viewlet>,
|
||||
CardRelationshipTable: '' as Ref<Viewlet>
|
||||
},
|
||||
string: {
|
||||
|
||||
@@ -184,6 +184,7 @@ const cardPlugin = plugin(cardId, {
|
||||
Feed: '' as Asset,
|
||||
All: '' as Asset,
|
||||
Duplicate: '' as Asset,
|
||||
Grid: '' as Asset,
|
||||
Lock: '' as Asset
|
||||
},
|
||||
extensions: {
|
||||
@@ -204,6 +205,7 @@ const cardPlugin = plugin(cardId, {
|
||||
AddTag: '' as IntlString,
|
||||
Feed: '' as IntlString,
|
||||
AllCards: '' as IntlString,
|
||||
Grid: '' as IntlString,
|
||||
Favorites: '' as IntlString,
|
||||
CreateCard: '' as IntlString,
|
||||
AllowCreatingCards: '' as IntlString,
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
<div class="flex-between flex-gap-2 h-4">
|
||||
<div class="flex-row-center flex-gap-2 font-regular-12">
|
||||
<PersonIdPresenter value={object.createdBy} noUnderline avatarSize="tiny" />
|
||||
<PersonIdPresenter value={object.createdBy} withPadding={false} noUnderline avatarSize="tiny" />
|
||||
<span>•</span>
|
||||
<span class="flex-no-shrink">
|
||||
<TimestampPresenter value={version?.lastModified ?? object.createdOn ?? object.modifiedOn} />
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
export let noUnderline = false
|
||||
export let shrink: boolean = false
|
||||
export let avatarSize: IconSize = 'x-small'
|
||||
export let withPadding: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
let person: Ref<Person> | undefined
|
||||
@@ -39,7 +40,7 @@
|
||||
</script>
|
||||
|
||||
{#if person}
|
||||
<div class="pl-3">
|
||||
<div class={withPadding ? 'pl-3' : ''}>
|
||||
<ObjectPresenter
|
||||
objectId={person}
|
||||
_class={contact.class.Person}
|
||||
|
||||
Reference in New Issue
Block a user