mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 21:50:34 +03:00
Link presenters init (#1579)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
83e8963e38
commit
d2f2a18376
@ -27,6 +27,7 @@ import type {
|
||||
HTMLPresenter,
|
||||
IgnoreActions,
|
||||
KeyBinding,
|
||||
LinkPresenter,
|
||||
ObjectEditor,
|
||||
ObjectEditorHeader,
|
||||
ObjectFactory,
|
||||
@ -199,6 +200,13 @@ export class TPreviewPresenter extends TClass implements PreviewPresenter {
|
||||
presenter!: AnyComponent
|
||||
}
|
||||
|
||||
@Model(view.class.LinkPresenter, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TLinkPresenter extends TDoc implements LinkPresenter {
|
||||
pattern!: string
|
||||
|
||||
component!: AnyComponent
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TAttributeEditor,
|
||||
@ -216,7 +224,8 @@ export function createModel (builder: Builder): void {
|
||||
TSpaceName,
|
||||
TTextPresenter,
|
||||
TIgnoreActions,
|
||||
TPreviewPresenter
|
||||
TPreviewPresenter,
|
||||
TLinkPresenter
|
||||
)
|
||||
|
||||
classPresenter(builder, core.class.TypeString, view.component.StringPresenter, view.component.StringEditor)
|
||||
@ -308,6 +317,16 @@ export function createModel (builder: Builder): void {
|
||||
singleInput: true
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.LinkPresenter, core.space.Model, {
|
||||
pattern: '(www.)?youtube.(com|ru)',
|
||||
component: view.component.YoutubePresenter
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.LinkPresenter, core.space.Model, {
|
||||
pattern: '(www.)?github.com/',
|
||||
component: view.component.GithubPresenter
|
||||
})
|
||||
|
||||
// Should be contributed via individual plugins.
|
||||
// actionTarget(builder, view.action.Open, core.class.Doc, { mode: ['browser', 'context'] })
|
||||
}
|
||||
|
@ -71,7 +71,9 @@ export default mergeIds(viewId, view, {
|
||||
DateEditor: '' as AnyComponent,
|
||||
DatePresenter: '' as AnyComponent,
|
||||
TableView: '' as AnyComponent,
|
||||
RolePresenter: '' as AnyComponent
|
||||
RolePresenter: '' as AnyComponent,
|
||||
YoutubePresenter: '' as AnyComponent,
|
||||
GithubPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Table: '' as IntlString,
|
||||
|
@ -42,6 +42,8 @@ export let nodes: NodeListOf<any>
|
||||
<br/>
|
||||
{:else if node.nodeName === 'HR'}
|
||||
<hr/>
|
||||
{:else if node.nodeName === 'IMG'}
|
||||
<div class="max-h-60 max-w-60 img">{@html node.outerHTML}</div>
|
||||
{:else if node.nodeName === 'H1'}
|
||||
<h1><svelte:self nodes={node.childNodes}/></h1>
|
||||
{:else if node.nodeName === 'H2'}
|
||||
@ -70,3 +72,13 @@ export let nodes: NodeListOf<any>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.img {
|
||||
:global(img) {
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
</style>
|
@ -437,6 +437,8 @@ p:last-child { margin-block-end: 0; }
|
||||
.min-w-min { min-width: min-content; }
|
||||
.min-h-0 { min-height: 0; }
|
||||
.max-h-125 { max-height: 31.25rem; }
|
||||
.max-h-60 { max-height: 15rem; }
|
||||
.max-w-60 { max-width: 15rem; }
|
||||
.clear-mins {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
@ -23,7 +23,7 @@
|
||||
import { Avatar, getClient, MessageViewer } from '@anticrm/presentation'
|
||||
import ui, { ActionIcon, IconMoreH, Menu, showPopup, Label, Tooltip, Button } from '@anticrm/ui'
|
||||
import { Action } from '@anticrm/view'
|
||||
import { getActions } from '@anticrm/view-resources'
|
||||
import { getActions, LinkPresenter } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
|
||||
import chunter from '../plugin'
|
||||
@ -152,6 +152,27 @@
|
||||
|
||||
$: parentMessage = message as Message
|
||||
$: hasReplies = (parentMessage?.replies?.length ?? 0) > 0
|
||||
|
||||
$: links = getLinks(message.content)
|
||||
|
||||
function getLinks (content: string): HTMLLinkElement[] {
|
||||
const parser = new DOMParser()
|
||||
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
|
||||
return parseLinks(parent.childNodes)
|
||||
}
|
||||
|
||||
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
|
||||
const res: HTMLLinkElement[] = []
|
||||
nodes.forEach((p) => {
|
||||
if (p.nodeType !== Node.TEXT_NODE) {
|
||||
if (p.nodeName === 'A') {
|
||||
res.push(p as HTMLLinkElement)
|
||||
}
|
||||
res.push(...parseLinks(p.childNodes))
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
@ -189,6 +210,9 @@
|
||||
<AttachmentList {attachments} {savedAttachmentsIds} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each links as link}
|
||||
<LinkPresenter {link}/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if reactions || (!thread && hasReplies)}
|
||||
<div class="footer flex-col">
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Role": "Role",
|
||||
"DeleteObject": "Delete object",
|
||||
"DeleteObjectConfirm": "Do you want to delete this {count, plural, =1 {object} other {# objects}}?",
|
||||
"Open": "Open"
|
||||
"Open": "Open",
|
||||
"Assignees": "Assignees",
|
||||
"Labels": "Labels"
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
"Role": "Роль",
|
||||
"DeleteObject": "Удалить объект",
|
||||
"DeleteObjectConfirm": "Вы действительно хотите удалить {count, plural, =1 {этот обьект} other {эти # обьекта}}?",
|
||||
"Open": "Открыть"
|
||||
"Open": "Открыть",
|
||||
"Assignees": "Исполнители",
|
||||
"Labels": "Метки"
|
||||
}
|
||||
}
|
||||
|
40
plugins/view-resources/src/components/LinkPresenter.svelte
Normal file
40
plugins/view-resources/src/components/LinkPresenter.svelte
Normal 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 { getClient } from "@anticrm/presentation"
|
||||
import { AnyComponent, Component } from "@anticrm/ui"
|
||||
import view from '../plugin'
|
||||
|
||||
export let link: HTMLLinkElement
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function getPresenter (href: string): Promise<AnyComponent | undefined> {
|
||||
const presenters = await client.findAll(view.class.LinkPresenter, {
|
||||
})
|
||||
for (const presenter of presenters) {
|
||||
if (new RegExp(presenter.pattern).test(href)) {
|
||||
return presenter.component
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await getPresenter(link.href) then presenter}
|
||||
{#if presenter}
|
||||
<Component is={presenter} props={{ href: link.href }} />
|
||||
{/if}
|
||||
{/await}
|
26
plugins/view-resources/src/components/icons/Play.svelte
Normal file
26
plugins/view-resources/src/components/icons/Play.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let size: 'small' | 'medium' | 'large' | 'full'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<g {fill}>
|
||||
<path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7 C27.852,25.902,29.798,24.778,31.356,25.677z"/>
|
||||
<path d="M69.981,47.977l-38.625-22.3c-0.233-0.134-0.474-0.21-0.716-0.259l37.341,21.559c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-0.349,0.201-0.716,0.288-1.078,0.301c0.656,0.938,1.961,1.343,3.078,0.699l38.625-22.3 C71.538,51.124,71.538,48.876,69.981,47.977z"/>
|
||||
<path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7C27.852,25.902,29.798,24.778,31.356,25.677z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/>
|
||||
</g>
|
||||
</svg>
|
@ -0,0 +1,102 @@
|
||||
<!--
|
||||
// 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 { MessageViewer } from '@anticrm/presentation'
|
||||
import { getPlatformColor, Label } from '@anticrm/ui'
|
||||
import view from '../../plugin'
|
||||
|
||||
export let href: string
|
||||
|
||||
interface Assignee {
|
||||
login: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
interface Label {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Data {
|
||||
number: string
|
||||
body: string | undefined
|
||||
title: string
|
||||
assignees: Assignee[]
|
||||
labels: Label[]
|
||||
}
|
||||
|
||||
async function getData (href: string): Promise<Data> {
|
||||
const params = href.replace(/(http.:\/\/)?(www.)?github.com\//, '')
|
||||
const res = await (await fetch(`https://api.github.com/repos/${params}`)).json()
|
||||
return {
|
||||
number: res.number,
|
||||
body: format(res.body),
|
||||
title: res.title,
|
||||
assignees: res.assignees,
|
||||
labels: res.labels
|
||||
}
|
||||
}
|
||||
|
||||
function format (body: string | undefined): string | undefined {
|
||||
if (!body) return undefined
|
||||
return body.replace(/[\r?\n]+/g, '<br />')
|
||||
.replace(/```(.+?)```/g, '<pre><code>$1</code></pre>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex mt-2">
|
||||
<div class="line" style="background-color: {getPlatformColor(7)}" />
|
||||
{#await getData(href) then data}
|
||||
<div class="flex-col">
|
||||
<a class="fs-title mb-1" {href}>#{data.number} {data.title}</a>
|
||||
{#if data.body}
|
||||
<div>
|
||||
<MessageViewer message={data.body} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-between">
|
||||
{#if data.assignees.length}
|
||||
<div class="flex-col">
|
||||
<div class="fs-title"><Label label={view.string.Assignees} /></div>
|
||||
<div>
|
||||
{#each data.assignees as assignee}
|
||||
<a href={assignee.html_url}>@{assignee.login}</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.labels.length}
|
||||
<div class="flex-col">
|
||||
<div class="fs-title"><Label label={view.string.Labels} /></div>
|
||||
<div>
|
||||
{#each data.labels as label, i}
|
||||
{#if i}, {/if}
|
||||
{label.name}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.line {
|
||||
margin-right: 1rem;
|
||||
width: 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,99 @@
|
||||
<!--
|
||||
// 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 Play from '../icons/Play.svelte'
|
||||
import { getPlatformColor } from '@anticrm/ui'
|
||||
|
||||
export let href: string
|
||||
const maxWidth = 400
|
||||
const maxHeight = 400
|
||||
let height = maxHeight
|
||||
let emb: HTMLDivElement | undefined
|
||||
|
||||
interface Data {
|
||||
author_url: string
|
||||
author: string
|
||||
thumbnail: string
|
||||
title: string
|
||||
html: string
|
||||
}
|
||||
|
||||
let played = false
|
||||
|
||||
async function getData (href: string): Promise<Data> {
|
||||
const res = await (await fetch(`https://www.youtube.com/oembed?url=${href}&format=json&maxwidth=${maxWidth}&maxheight=${maxHeight}`)).json()
|
||||
height = (res.thumbnail_height / res.thumbnail_width) * maxWidth
|
||||
return {
|
||||
author_url: res.author_url,
|
||||
author: res.author_name,
|
||||
thumbnail: res.thumbnail_url,
|
||||
title: res.title,
|
||||
html: res.html
|
||||
}
|
||||
}
|
||||
|
||||
function setHeigh (emb: HTMLElement): void {
|
||||
const child = (emb.firstElementChild as HTMLElement)
|
||||
child.style.height = `${height}px`
|
||||
child.setAttribute('height', `${height}px`)
|
||||
}
|
||||
|
||||
$: emb && setHeigh(emb)
|
||||
</script>
|
||||
|
||||
<div class="flex mt-2">
|
||||
<div class="line" style="background-color: {getPlatformColor(2)}" />
|
||||
{#await getData(href) then data}
|
||||
<div class="flex-col">
|
||||
<div class="mb-1"><a class="fs-title" {href} >{data.title}</a></div>
|
||||
<div class="mb-1"><a href={data.author_url} >{data.author}</a></div>
|
||||
{#if !played}
|
||||
<div
|
||||
class="container"
|
||||
on:click={() => {
|
||||
played = true
|
||||
}}
|
||||
>
|
||||
<img width="400px" src={data.thumbnail} alt={data.title}/>
|
||||
<div class="play-btn"><Play size={'full'} /></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div bind:this={emb}>
|
||||
{@html data.html}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.line {
|
||||
margin-right: 1rem;
|
||||
width: 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
top: calc(50% - 50px);
|
||||
left: calc(50% - 50px);
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -35,12 +35,15 @@ import Table from './components/Table.svelte'
|
||||
import TableView from './components/TableView.svelte'
|
||||
import TimestampPresenter from './components/TimestampPresenter.svelte'
|
||||
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
|
||||
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
|
||||
|
||||
export { getActions } from './actions'
|
||||
export { default as ActionContext } from './components/ActionContext.svelte'
|
||||
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
||||
export { default as ContextMenu } from './components/Menu.svelte'
|
||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
|
||||
export * from './context'
|
||||
export * from './selection'
|
||||
export { buildModel, getCollectionCounter, getObjectPresenter, LoadingProps } from './utils'
|
||||
@ -64,6 +67,8 @@ export default async (): Promise<Resources> => ({
|
||||
ObjectPresenter,
|
||||
EditDoc,
|
||||
HTMLPresenter,
|
||||
IntlStringPresenter
|
||||
IntlStringPresenter,
|
||||
GithubPresenter,
|
||||
YoutubePresenter
|
||||
}
|
||||
})
|
||||
|
@ -28,6 +28,8 @@ export default mergeIds(viewId, view, {
|
||||
LabelNA: '' as IntlString,
|
||||
ChooseAColor: '' as IntlString,
|
||||
DeleteObject: '' as IntlString,
|
||||
DeleteObjectConfirm: '' as IntlString
|
||||
DeleteObjectConfirm: '' as IntlString,
|
||||
Assignees: '' as IntlString,
|
||||
Labels: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -97,6 +97,14 @@ export interface Viewlet extends Doc {
|
||||
config: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface LinkPresenter extends Doc {
|
||||
pattern: string
|
||||
component: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
@ -264,7 +272,8 @@ const view = plugin(viewId, {
|
||||
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,
|
||||
Viewlet: '' as Ref<Class<Viewlet>>,
|
||||
Action: '' as Ref<Class<Action>>,
|
||||
ActionTarget: '' as Ref<Class<ActionTarget>>
|
||||
ActionTarget: '' as Ref<Class<ActionTarget>>,
|
||||
LinkPresenter: '' as Ref<Class<LinkPresenter>>
|
||||
},
|
||||
viewlet: {
|
||||
Table: '' as Ref<ViewletDescriptor>
|
||||
|
Loading…
Reference in New Issue
Block a user