mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
UBER-942: Few skill fixes (#3971)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
f8cdaba5f2
commit
25f0e4d579
@ -17,7 +17,8 @@
|
|||||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { Card, getClient } from '@hcengineering/presentation'
|
import { Card, getClient } from '@hcengineering/presentation'
|
||||||
import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
|
import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
|
||||||
import { EditBox, ListView, Loading } from '@hcengineering/ui'
|
import { Button, CheckBox, EditBox, Lazy, ListView, Loading } from '@hcengineering/ui'
|
||||||
|
import Expandable from '@hcengineering/ui/src/components/Expandable.svelte'
|
||||||
import { FILTER_DEBOUNCE_MS } from '@hcengineering/view-resources'
|
import { FILTER_DEBOUNCE_MS } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import recruit from '../plugin'
|
import recruit from '../plugin'
|
||||||
@ -77,7 +78,8 @@
|
|||||||
element?: TagElement
|
element?: TagElement
|
||||||
move: Ref<TagElement>[]
|
move: Ref<TagElement>[]
|
||||||
toDelete: boolean
|
toDelete: boolean
|
||||||
total?: number
|
total: number
|
||||||
|
newRefs: number
|
||||||
}[]
|
}[]
|
||||||
move: number
|
move: number
|
||||||
}
|
}
|
||||||
@ -157,8 +159,54 @@
|
|||||||
elements: [],
|
elements: [],
|
||||||
move: 0
|
move: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let expertRefs: Pick<TagReference, '_id' | '_class' | 'tag' | 'title'>[] = []
|
||||||
|
|
||||||
|
let titles: string[] = []
|
||||||
|
const titlesStates: Map<string, boolean> = new Map()
|
||||||
|
$: getClient()
|
||||||
|
.findAll(
|
||||||
|
tags.class.TagReference,
|
||||||
|
{
|
||||||
|
tag: {
|
||||||
|
$in: Array.from(elements.map((it) => it._id))
|
||||||
|
},
|
||||||
|
weight: { $gt: 5 } // We need expert ones.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projection: {
|
||||||
|
tag: 1,
|
||||||
|
_id: 1,
|
||||||
|
title: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
expertRefs = res
|
||||||
|
})
|
||||||
|
|
||||||
|
$: preparedRefs = expertRefs.map((it) => ({ ...it, title: prepareTitle(it.title) }))
|
||||||
|
|
||||||
|
let counters: Map<string, number> = new Map()
|
||||||
|
$: {
|
||||||
|
const _counters: Map<string, number> = new Map()
|
||||||
|
for (const t of titles) {
|
||||||
|
const refs = preparedRefs.filter((it) => it.title.toLowerCase() === t).length
|
||||||
|
_counters.set(t, refs)
|
||||||
|
if (refs < 5) {
|
||||||
|
titlesStates.set(t, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counters = _counters
|
||||||
|
}
|
||||||
|
|
||||||
|
$: titles = Array.from(new Set(expertRefs.map((it) => prepareTitle(it.title.toLocaleLowerCase()))))
|
||||||
|
|
||||||
// Will return a set of operations over tag elements
|
// Will return a set of operations over tag elements
|
||||||
async function updateTagsList (tagElements: TagElement[]): Promise<void> {
|
async function updateTagsList (
|
||||||
|
tagElements: TagElement[],
|
||||||
|
expertRefs: Pick<TagReference, '_id' | '_class' | 'tag' | 'title'>[]
|
||||||
|
): Promise<void> {
|
||||||
const _plan: TagUpdatePlan = {
|
const _plan: TagUpdatePlan = {
|
||||||
elements: [],
|
elements: [],
|
||||||
move: 0
|
move: 0
|
||||||
@ -175,23 +223,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expertRefs = await getClient().findAll(
|
|
||||||
tags.class.TagReference,
|
|
||||||
{
|
|
||||||
tag: {
|
|
||||||
$in: Array.from(tagElements.map((it) => it._id))
|
|
||||||
},
|
|
||||||
weight: { $gt: 5 } // We need expert ones.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
projection: {
|
|
||||||
tag: 1,
|
|
||||||
_id: 1,
|
|
||||||
title: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const toGoodTags = Array.from(new Set(expertRefs.map((it) => it.tag)))
|
const toGoodTags = Array.from(new Set(expertRefs.map((it) => it.tag)))
|
||||||
.map((it) => tagMap.get(it))
|
.map((it) => tagMap.get(it))
|
||||||
.filter((it) => it) as TagElement[]
|
.filter((it) => it) as TagElement[]
|
||||||
@ -211,15 +242,34 @@
|
|||||||
.filter((t) => t.title.length > 2)
|
.filter((t) => t.title.length > 2)
|
||||||
const goodSortedTagsTitles = new Map<Ref<TagElement>, string>()
|
const goodSortedTagsTitles = new Map<Ref<TagElement>, string>()
|
||||||
processed = -1
|
processed = -1
|
||||||
|
|
||||||
|
// Candidate to have in list.
|
||||||
|
const allRefs = await getClient().findAll(
|
||||||
|
tags.class.TagReference,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
projection: {
|
||||||
|
tag: 1,
|
||||||
|
_id: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const tagElementIds = new Map<Ref<TagElement>, TagUpdatePlan['elements'][0]>()
|
||||||
|
|
||||||
for (const tag of tagElements.toSorted((a, b) => prepareTitle(a.title).length - prepareTitle(b.title).length)) {
|
for (const tag of tagElements.toSorted((a, b) => prepareTitle(a.title).length - prepareTitle(b.title).length)) {
|
||||||
processed++
|
processed++
|
||||||
|
const refs = allRefs.filter((it) => it.tag === tag._id)
|
||||||
if (goodTagMap.has(tag._id)) {
|
if (goodTagMap.has(tag._id)) {
|
||||||
_plan.elements.push({
|
const ee = {
|
||||||
original: tag,
|
original: tag,
|
||||||
move: [],
|
move: [],
|
||||||
toDelete: false,
|
toDelete: false,
|
||||||
total: -1
|
total: refs.length,
|
||||||
})
|
newRefs: 0
|
||||||
|
}
|
||||||
|
_plan.elements.push(ee)
|
||||||
|
tagElementIds.set(tag._id, ee)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let title = prepareTitle(tag.title)
|
let title = prepareTitle(tag.title)
|
||||||
@ -227,7 +277,9 @@
|
|||||||
_plan.elements.push({
|
_plan.elements.push({
|
||||||
original: tag,
|
original: tag,
|
||||||
move: [],
|
move: [],
|
||||||
toDelete: true
|
toDelete: true,
|
||||||
|
total: refs.length,
|
||||||
|
newRefs: 0
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -236,8 +288,16 @@
|
|||||||
_plan.elements.push({
|
_plan.elements.push({
|
||||||
original: tag,
|
original: tag,
|
||||||
move: namedIdx !== undefined ? [namedIdx] : [],
|
move: namedIdx !== undefined ? [namedIdx] : [],
|
||||||
toDelete: true
|
toDelete: true,
|
||||||
|
total: refs.length,
|
||||||
|
newRefs: 0
|
||||||
})
|
})
|
||||||
|
if (namedIdx !== undefined) {
|
||||||
|
const re = tagElementIds.get(namedIdx)
|
||||||
|
if (re !== undefined) {
|
||||||
|
re.newRefs += refs.length
|
||||||
|
}
|
||||||
|
}
|
||||||
_plan.move++
|
_plan.move++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -271,7 +331,9 @@
|
|||||||
const mve: TagUpdatePlan['elements'][0] = {
|
const mve: TagUpdatePlan['elements'][0] = {
|
||||||
original: tag,
|
original: tag,
|
||||||
move: [],
|
move: [],
|
||||||
toDelete: false
|
toDelete: false,
|
||||||
|
newRefs: 0,
|
||||||
|
total: refs.length
|
||||||
}
|
}
|
||||||
for (const t of toReplace) {
|
for (const t of toReplace) {
|
||||||
let tt = goodSortedTagsTitles.get(t._id)
|
let tt = goodSortedTagsTitles.get(t._id)
|
||||||
@ -293,6 +355,11 @@
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mve.move.push(t._id)
|
mve.move.push(t._id)
|
||||||
|
|
||||||
|
const re = tagElementIds.get(t._id)
|
||||||
|
if (re !== undefined) {
|
||||||
|
re.newRefs += mve.total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const namedIdx = namedElements.get(prepareTitle(title).toLowerCase())
|
const namedIdx = namedElements.get(prepareTitle(title).toLowerCase())
|
||||||
if (namedIdx !== undefined) {
|
if (namedIdx !== undefined) {
|
||||||
@ -302,14 +369,13 @@
|
|||||||
mve.element = { ...tag, title: prepareTitle(title) }
|
mve.element = { ...tag, title: prepareTitle(title) }
|
||||||
mve.toDelete = prepareTitle(title).length <= 1 || isForRemove(title)
|
mve.toDelete = prepareTitle(title).length <= 1 || isForRemove(title)
|
||||||
|
|
||||||
|
mve.total = refs.length
|
||||||
if (isForRemove(title)) {
|
if (isForRemove(title)) {
|
||||||
mve.element.title = ''
|
mve.element.title = ''
|
||||||
} else {
|
} else {
|
||||||
// Candidate to have in list.
|
// Candidate to have in list.
|
||||||
const refs = await getClient().findAll(tags.class.TagReference, { tag: tag._id }, { limit: 2, total: true })
|
|
||||||
if (refs.length < 2) {
|
if (refs.length < 2) {
|
||||||
mve.toDelete = true
|
mve.toDelete = true
|
||||||
mve.total = refs.total
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!mve.toDelete) {
|
if (!mve.toDelete) {
|
||||||
@ -320,49 +386,62 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_plan.elements.push(mve)
|
_plan.elements.push(mve)
|
||||||
|
tagElementIds.set(tag._id, mve)
|
||||||
_plan.move++
|
_plan.move++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Candidate to have in list.
|
// Candidate to have in list.
|
||||||
const refs = await getClient().findAll(tags.class.TagReference, { tag: tag._id }, { limit: 2, total: true })
|
|
||||||
if (isForRemove(title) || refs.length < 2) {
|
if (isForRemove(title) || refs.length < 2) {
|
||||||
_plan.elements.push({
|
_plan.elements.push({
|
||||||
original: tag,
|
original: tag,
|
||||||
move: [],
|
move: [],
|
||||||
toDelete: true
|
toDelete: true,
|
||||||
|
newRefs: 0,
|
||||||
|
total: refs.length
|
||||||
})
|
})
|
||||||
_plan.move++
|
_plan.move++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
namedElements.set(prepareTitle(title.toLowerCase()), tag._id)
|
namedElements.set(prepareTitle(title.toLowerCase()), tag._id)
|
||||||
const ee = {
|
const ee: TagUpdatePlan['elements'][0] = {
|
||||||
original: tag,
|
original: tag,
|
||||||
element: { ...tag, title },
|
element: { ...tag, title },
|
||||||
move: [],
|
move: [],
|
||||||
toDelete: false,
|
toDelete: false,
|
||||||
total: refs.total
|
total: refs.length,
|
||||||
|
newRefs: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
_plan.elements.push(ee)
|
_plan.elements.push(ee)
|
||||||
if (ee.element?.title.length > 2) {
|
if (ee.element !== undefined && ee.element.title.length > 2) {
|
||||||
goodSortedTags.push(ee.element)
|
goodSortedTags.push(ee.element)
|
||||||
goodSortedTagsTitles.delete(ee.element._id)
|
goodSortedTagsTitles.delete(ee.element._id)
|
||||||
goodSortedTags.sort((a, b) => b.title.length - a.title.length).filter((t) => t.title.length > 2)
|
goodSortedTags.sort((a, b) => b.title.length - a.title.length).filter((t) => t.title.length > 2)
|
||||||
}
|
}
|
||||||
|
if (ee.element !== undefined) {
|
||||||
goodTags.push(ee.element)
|
goodTags.push(ee.element)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_plan.elements.sort((a, b) => prepareTitle(a.original.title).localeCompare(prepareTitle(b.original.title)))
|
_plan.elements.sort((a, b) => prepareTitle(a.original.title).localeCompare(prepareTitle(b.original.title)))
|
||||||
plan = _plan
|
plan = _plan
|
||||||
processed = 0
|
processed = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let doProcessing = false
|
let doProcessing = false
|
||||||
$: {
|
|
||||||
|
function doAnalyse (): void {
|
||||||
doProcessing = true
|
doProcessing = true
|
||||||
updateTagsList(elements).then(() => {
|
if (elements.length > 0 && expertRefs.length > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
updateTagsList(
|
||||||
|
elements,
|
||||||
|
expertRefs.filter((it) => titlesStates.get(prepareTitle(it.title.toLowerCase())) ?? true)
|
||||||
|
).then(() => {
|
||||||
doProcessing = false
|
doProcessing = false
|
||||||
})
|
})
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let search: string = ''
|
let search: string = ''
|
||||||
|
|
||||||
@ -376,6 +455,10 @@
|
|||||||
(it) => it.original.title.toLowerCase().indexOf(_search.toLowerCase()) !== -1
|
(it) => it.original.title.toLowerCase().indexOf(_search.toLowerCase()) !== -1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$: idMap = new Map(plan.elements.filter((it) => it.toDelete === false).map((it) => [it.original._id, it]))
|
||||||
|
|
||||||
|
$: searchTitles = titles.filter((it) => it.toLowerCase().indexOf(_search.toLowerCase()) !== -1)
|
||||||
|
|
||||||
let processed: number = 0
|
let processed: number = 0
|
||||||
|
|
||||||
async function applyPlan (): Promise<void> {
|
async function applyPlan (): Promise<void> {
|
||||||
@ -407,7 +490,7 @@
|
|||||||
// We need to find all objects and add new tag elements to them and preserve skill level
|
// We need to find all objects and add new tag elements to them and preserve skill level
|
||||||
for (const a of allRefs) {
|
for (const a of allRefs) {
|
||||||
for (const m of item.move) {
|
for (const m of item.move) {
|
||||||
const me = plan.elements.find((it) => it.original._id === m && it.toDelete === false)
|
const me = idMap.get(m)
|
||||||
if (me !== undefined) {
|
if (me !== undefined) {
|
||||||
const id = await ops.addCollection(
|
const id = await ops.addCollection(
|
||||||
tags.class.TagReference,
|
tags.class.TagReference,
|
||||||
@ -486,6 +569,57 @@
|
|||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportCSV (name: string, data: string): void {
|
||||||
|
const filename = name + new Date().toLocaleDateString() + '.csv'
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.style.display = 'none'
|
||||||
|
link.setAttribute('target', '_blank')
|
||||||
|
link.setAttribute('href', 'data:text/csv;charset=utf-8,%EF%BB%BF' + encodeURIComponent(data))
|
||||||
|
link.setAttribute('download', filename)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportExpertSkills (): string {
|
||||||
|
// Construct csv
|
||||||
|
const csv: string[] = []
|
||||||
|
csv.push('title;enabled;references')
|
||||||
|
for (const t of titles) {
|
||||||
|
const row: string[] = []
|
||||||
|
row.push('"' + t + '"')
|
||||||
|
row.push('"' + (titlesStates.get(t) ?? true) ? 'true' : 'false' + '"')
|
||||||
|
row.push('"' + (counters.get(t) ?? 0) + '"')
|
||||||
|
csv.push(row.join(';'))
|
||||||
|
}
|
||||||
|
return csv.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPlan (): string {
|
||||||
|
// Construct csv
|
||||||
|
const csv: string[] = []
|
||||||
|
csv.push('number; title;total; new refs; new title;to delete;will add tags ')
|
||||||
|
let i = 0
|
||||||
|
for (const t of plan.elements) {
|
||||||
|
const row: string[] = []
|
||||||
|
row.push('"' + i++ + '"')
|
||||||
|
row.push('"' + (t.original?.title ?? '') + '"')
|
||||||
|
row.push('"' + (t.total + t.newRefs) + '"')
|
||||||
|
row.push('"' + t.newRefs + '"')
|
||||||
|
row.push('"' + (t.element?.title ?? '') + '"')
|
||||||
|
row.push('"' + (t.toDelete ? 'yes' : '') + '"')
|
||||||
|
const moveTo = t.move.map((it) => {
|
||||||
|
const orig = idMap.get(it)
|
||||||
|
return (
|
||||||
|
orig?.original.title + (' (#' + plan.elements.findIndex((it) => it.original._id === orig?.original._id) + ')')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
row.push('"' + moveTo.join(', ') + '"')
|
||||||
|
csv.push(row.join(';'))
|
||||||
|
}
|
||||||
|
return csv.join('\n')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
@ -500,8 +634,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<EditBox kind={'search-style'} bind:value={search} />
|
<EditBox kind={'search-style'} bind:value={search} />
|
||||||
|
{#if processed > 0 || doProcessing}
|
||||||
{#if processed > 0}
|
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<Loading />
|
<Loading />
|
||||||
Processing: {processed} / {searchPlanElements.length}
|
Processing: {processed} / {searchPlanElements.length}
|
||||||
@ -511,35 +644,86 @@
|
|||||||
<Loading />
|
<Loading />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<Expandable>
|
||||||
|
<svelte:fragment slot="title">
|
||||||
|
Existing Expert level skills
|
||||||
|
{titles.length} =>
|
||||||
|
{titles.filter((it) => titlesStates.get(it) ?? true).length}
|
||||||
|
</svelte:fragment>
|
||||||
|
<div class="h-60" style:overflow={'auto'}>
|
||||||
|
<ListView count={searchTitles.length}>
|
||||||
|
<svelte:fragment slot="item" let:item>
|
||||||
|
{@const el = searchTitles[item]}
|
||||||
|
<div class="flex-row-center flex-nowrap no-word-wrap">
|
||||||
|
<CheckBox
|
||||||
|
checked={titlesStates.get(el) ?? true}
|
||||||
|
on:value={(val) => {
|
||||||
|
titlesStates.set(el, val.detail === true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{el}
|
||||||
|
{counters.get(el)}
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ListView>
|
||||||
|
</div>
|
||||||
|
</Expandable>
|
||||||
|
<Expandable>
|
||||||
|
<svelte:fragment slot="title">
|
||||||
|
<div class="flex-row-center">
|
||||||
|
Update plan {elements.length}
|
||||||
|
|
||||||
|
{#if plan.elements.length - plan.move > 0}
|
||||||
|
=> {plan.elements.length - plan.move}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
<div class="h-60" style:overflow={'auto'}>
|
||||||
|
{#if plan.elements.length > 0}
|
||||||
<div class="flex clear-mins" style:overflow={'auto'}>
|
<div class="flex clear-mins" style:overflow={'auto'}>
|
||||||
<div class="flex-grow flex-nowrap no-word-wrap">
|
<div class="flex-grow flex-nowrap no-word-wrap">
|
||||||
<div class="flex-row-center">
|
|
||||||
{elements.length} => {plan.elements.length - plan.move}
|
|
||||||
</div>
|
|
||||||
<ListView count={searchPlanElements.length}>
|
<ListView count={searchPlanElements.length}>
|
||||||
<svelte:fragment slot="item" let:item>
|
<svelte:fragment slot="item" let:item>
|
||||||
{@const el = searchPlanElements[item]}
|
{@const el = searchPlanElements[item]}
|
||||||
<div class="flex-row-center" style:color={toColor(el)}>
|
<div class="flex-row-center" style:color={toColor(el)}>
|
||||||
|
<Lazy>
|
||||||
{el.original.title}
|
{el.original.title}
|
||||||
{#if el.element}
|
{#if el.element}
|
||||||
=> {el.element?.title}
|
=> {el.element?.title}
|
||||||
{/if}
|
{/if}
|
||||||
{#each el.move as mid}
|
{#each el.move as mid}
|
||||||
{@const orig = plan.elements.find((it) => it.original._id === mid)}
|
{@const orig = idMap.get(mid)}
|
||||||
{#if orig !== undefined}
|
{#if orig !== undefined}
|
||||||
➡︎ {orig?.element?.title ?? orig?.original.title}
|
➡︎ {orig?.element?.title ?? orig?.original.title}
|
||||||
{:else}
|
{:else}
|
||||||
{mid}
|
{mid}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if (el.total ?? 0) > 0}
|
({el.total + el.newRefs}) {el.newRefs}
|
||||||
({el.total})
|
</Lazy>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Expandable>
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<Button label={getEmbeddedLabel('Analyse')} on:click={() => doAnalyse()} />
|
||||||
|
<Button
|
||||||
|
label={getEmbeddedLabel('Export expert skills')}
|
||||||
|
on:click={() => {
|
||||||
|
exportCSV('experts', exportExpertSkills())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={getEmbeddedLabel('Export plan')}
|
||||||
|
on:click={() => {
|
||||||
|
exportCSV('plan', exportPlan())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
Loading…
Reference in New Issue
Block a user