Recognize attached document properties on Candidate creation (#904)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-02 16:03:29 +07:00 committed by GitHub
parent d45cf8ccfe
commit 5b644aac5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 422 additions and 67 deletions

View File

@ -80,6 +80,7 @@ specifiers:
'@rush-temp/recruit': file:./projects/recruit.tgz
'@rush-temp/recruit-assets': file:./projects/recruit-assets.tgz
'@rush-temp/recruit-resources': file:./projects/recruit-resources.tgz
'@rush-temp/rekoni': file:./projects/rekoni.tgz
'@rush-temp/server': file:./projects/server.tgz
'@rush-temp/server-attachment': file:./projects/server-attachment.tgz
'@rush-temp/server-attachment-resources': file:./projects/server-attachment-resources.tgz
@ -274,6 +275,7 @@ dependencies:
'@rush-temp/recruit': file:projects/recruit.tgz
'@rush-temp/recruit-assets': file:projects/recruit-assets.tgz
'@rush-temp/recruit-resources': file:projects/recruit-resources.tgz_096c09b0b673a57c275d9767a12070b1
'@rush-temp/rekoni': file:projects/rekoni.tgz_096c09b0b673a57c275d9767a12070b1
'@rush-temp/server': file:projects/server.tgz
'@rush-temp/server-attachment': file:projects/server-attachment.tgz
'@rush-temp/server-attachment-resources': file:projects/server-attachment-resources.tgz
@ -10136,6 +10138,12 @@ packages:
resolution: {integrity: sha1-fRh9tcbNu9ZNdaMvkbiZi94yc8M=}
dev: false
/svelte-hmr/0.14.7:
resolution: {integrity: sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==}
peerDependencies:
svelte: '>=3.19.0'
dev: false
/svelte-hmr/0.14.7_svelte@3.44.3:
resolution: {integrity: sha512-pDrzgcWSoMaK6AJkBWkmgIsecW0GChxYZSZieIYfCP0v2oPyx2CYU/zm7TBIcjLVUPP714WxmViE9Thht4etog==}
peerDependencies:
@ -10144,6 +10152,16 @@ packages:
svelte: 3.44.3
dev: false
/svelte-loader/3.1.2:
resolution: {integrity: sha512-RhVIvitb+mtIwKNyvNQoDQ0EhXg2KH8LhQiiqeJh8u6vqJyGWoMoFcYCar69TT+1iaK5IYe0wPNYJ6TILcsurw==}
peerDependencies:
svelte: '>3.0.0'
dependencies:
loader-utils: 2.0.2
svelte-dev-helper: 1.1.9
svelte-hmr: 0.14.7
dev: false
/svelte-loader/3.1.2_svelte@3.44.3:
resolution: {integrity: sha512-RhVIvitb+mtIwKNyvNQoDQ0EhXg2KH8LhQiiqeJh8u6vqJyGWoMoFcYCar69TT+1iaK5IYe0wPNYJ6TILcsurw==}
peerDependencies:
@ -11983,7 +12001,7 @@ packages:
dev: false
file:projects/front.tgz:
resolution: {integrity: sha512-Nto3Qer5qe5YaIELZhEaJugu6x/1SbThjaKd0Yyc5BCo6UfjeyvQyKz0iHmTtIVfbBfzQSqj+MMBGL0k6zW3dg==, tarball: file:projects/front.tgz}
resolution: {integrity: sha512-RXsa4jlZB6UdPjSIAHmf07BEcWlH6N26QnAVFQ3QL5VdqLi73ohsPQV9seKz36c5jGsA//Z0BS9QYVCETuHdgA==, tarball: file:projects/front.tgz}
name: '@rush-temp/front'
version: 0.0.0
dependencies:
@ -13065,7 +13083,7 @@ packages:
dev: false
file:projects/prod.tgz_sass@1.45.0+typescript@4.5.4:
resolution: {integrity: sha512-E5QAAHRWBXbn+0MENtAgD6sM4IVzdCzqJHYlEzaw5TUIUZkcyCs+NRLEFBSW/uP1nfOmScrJOyFPiG59FFCeDQ==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-mb0NOzQOQI/mZjzWLO+zp8F57x5koGPRF0qdrONOBHgKJ+wKpbSrVyW911S0nM8b/mVimVqaXCI2XdjAvYO1mg==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
@ -13195,6 +13213,41 @@ packages:
- supports-color
dev: false
file:projects/rekoni.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-y0ghO9RbY5FeFqBM/g2AE2GWIxvfbXXDZUFgl9/QDSKfX6/G3xr2JdsSXMXUr82YAi7xRRpnIkUHmU4Zws7z1Q==, tarball: file:projects/rekoni.tgz}
id: file:projects/rekoni.tgz
name: '@rush-temp/rekoni'
version: 0.0.0
dependencies:
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
eslint-plugin-svelte3: 3.2.1_eslint@7.32.0
prettier: 2.5.1
prettier-plugin-svelte: 2.5.1_prettier@2.5.1
sass: 1.45.0
svelte-check: 2.3.0_ac194b5590200ebf8338e0f86ec190f4
svelte-loader: 3.1.2
svelte-preprocess: 4.10.1_3ae2e5fc7d8fb60bbcea513ad0b15c0f
typescript: 4.5.4
transitivePeerDependencies:
- '@babel/core'
- coffeescript
- less
- node-sass
- postcss
- postcss-load-config
- pug
- stylus
- sugarss
- supports-color
- svelte
dev: false
file:projects/server-attachment-resources.tgz:
resolution: {integrity: sha512-V/H2gWfte5sRJYj91+6StCv/+q2vAp6iHQRBcwBjcFJwMyuFqAQMhaGsWvJtAgtIENRY/22/I8KWiwg8O74XCg==, tarball: file:projects/server-attachment-resources.tgz}
name: '@rush-temp/server-attachment-resources'

View File

@ -5,3 +5,5 @@ LOGIN_TOKEN_DEV=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InJvc2FtdW5kQGh
LOGIN_ENDPOINT_DEV=wss://transactor.hc.engineering/
TELEGRAM_URL=http://localhost:8086
REKONI_URL=http://localhost:4004

View File

@ -1,2 +1,3 @@
TELEGRAM_URL = https://telegram.hc.engineering
REKONI_URL = https://rekini.hc.engineering

View File

@ -1 +1,5 @@
{"ACCOUNTS_URL":"http://localhost:3000","UPLOAD_URL":"/files"}
{
"ACCOUNTS_URL":"http://localhost:3000",
"UPLOAD_URL":"/files",
"REKONI_URL": "http://localhost:4004"
}

View File

@ -97,6 +97,7 @@
"@anticrm/templates": "~0.6.0",
"@anticrm/templates-assets": "~0.6.0",
"@anticrm/templates-resources": "~0.6.0",
"@anticrm/core": "~0.6.16"
"@anticrm/core": "~0.6.16",
"@anticrm/rekoni": "~0.6.0"
}
}

View File

@ -33,6 +33,7 @@ import { gmailId } from '@anticrm/gmail'
import { imageCropperId } from '@anticrm/image-cropper'
import { inventoryId } from '@anticrm/inventory'
import { templatesId } from '@anticrm/templates'
import rekoni from '@anticrm/rekoni'
import '@anticrm/login-assets'
import '@anticrm/task-assets'
@ -57,6 +58,7 @@ export async function configurePlatform() {
setMetadata(login.metadata.AccountsUrl, config.ACCOUNTS_URL)
setMetadata(login.metadata.UploadUrl, config.UPLOAD_URL)
if( config.MODEL_VERSION != null) {
console.log('Minimal Model version requirement', config.MODEL_VERSION)
setMetadata(workbench.metadata.RequiredVersion, config.MODEL_VERSION)
@ -65,6 +67,8 @@ export async function configurePlatform() {
setMetadata(login.metadata.GmailUrl, process.env.GMAIL_URL ?? 'http://localhost:8087')
setMetadata(login.metadata.OverrideEndpoint, process.env.LOGIN_ENDPOINT)
setMetadata(rekoni.metadata.RekoniUrl, process.env.REKONI_URL)
addLocation(clientId, () => import(/* webpackChunkName: "client" */ '@anticrm/client-resources'))
addLocation(loginId, () => import(/* webpackChunkName: "login" */ '@anticrm/login-resources'))
addLocation(workbenchId, () => import(/* webpackChunkName: "workbench" */ '@anticrm/workbench-resources'))

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { Doc, Ref } from '@anticrm/core'
import type { AttachedData, Doc, Ref } from '@anticrm/core'
import type { IntlString, Asset } from '@anticrm/platform'
import type { Channel, ChannelProvider } from '@anticrm/contact'
import { getClient } from '..'
@ -26,7 +26,7 @@
import contact from '@anticrm/contact'
import { createEventDispatcher } from 'svelte'
export let value: Channel[] | null
export let value: AttachedData<Channel>[] | null
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
export let reverse: boolean = false
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
@ -51,7 +51,7 @@
return map
}
async function update (value: Channel[]) {
async function update (value: AttachedData<Channel>[]) {
const result = []
const map = await getProviders()
for (const item of value) {

View File

@ -23,6 +23,7 @@
export let avatar: string | undefined = undefined
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'
export let direct: Blob | undefined = undefined
const dispatch = createEventDispatcher()
@ -32,7 +33,6 @@
inputRef.click()
}
let direct: Blob | undefined
function onSelect (e: any) {
const file = e.target?.files[0] as File | undefined
@ -48,7 +48,7 @@
direct = blob
dispatch('done', { file: new File([blob], file.name) })
})
e.target.value = null;
e.target.value = null
}
</script>

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/ui/config/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname },
settings: {
'svelte3/ignore-styles': () => true
}
}

View File

@ -0,0 +1,37 @@
{
"name": "@anticrm/rekoni",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "echo 'no build for ui'",
"build:docs": "api-extractor run --local",
"lint": "svelte-check && eslint",
"lint:fix": "eslint --fix src",
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src",
"svelte-check": "svelte-check"
},
"devDependencies": {
"svelte-loader": "^3.1.2",
"sass": "^1.37.5",
"svelte-preprocess": "^4.7.4",
"@anticrm/platform-rig": "~0.6.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-svelte3": "~3.2.1",
"prettier-plugin-svelte": "^2.2.0",
"eslint": "^7.32.0",
"prettier": "^2.4.1",
"svelte-check": "^2.2.10",
"typescript": "^4.3.5"
},
"dependencies": {
"@anticrm/platform": "~0.6.5",
"@anticrm/core": "~0.6.11"
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}

View File

@ -0,0 +1,39 @@
//
// 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.
//
import { getMetadata, PlatformError, unknownError } from '@anticrm/platform'
import plugin from './plugin'
import { ReconiDocument } from './types'
export { default } from './plugin'
export * from './types'
export async function recognizeDocument (token: string, url: string): Promise<ReconiDocument> {
const rekoniUrl = getMetadata(plugin.metadata.RekoniUrl)
if (rekoniUrl === undefined) {
// We could try use recognition service to find some document properties.
throw new PlatformError(unknownError('recognition framework is not configured'))
}
return await (await fetch(rekoniUrl + '/recognize?format=pdf', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileUrl: url
})
})).json() as ReconiDocument
}

View File

@ -0,0 +1,28 @@
//
// 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.
//
import type { Metadata, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
/**
* @public
*/
export const rekoniId = 'rekoni' as Plugin
export default plugin(rekoniId, {
metadata: {
RekoniUrl: '' as Metadata<string>
}
})

View File

@ -0,0 +1,33 @@
//
// 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.
//
/**
* @public
*/
export interface ReconiDocument {
firstName: string
lastName: string
avatar?: string
email?: string
phone?: string
city?: string
skype?: string
linkedin?: string
gmail?: string
github?: string
facebook?: string
telegram?: string
twitter?: string
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
"declaration": true,
"outDir": "./lib",
"strict": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
]
}
}

View File

@ -15,20 +15,20 @@
-->
<script lang="ts">
import { Channel } from '@anticrm/contact'
import type { Doc,Ref } from '@anticrm/core'
import presentation,{ Channels } from '@anticrm/presentation'
import { CircleButton,IconAdd,Label,showPopup } from '@anticrm/ui'
import type { AttachedData, Doc, Ref } from '@anticrm/core'
import presentation, { Channels } from '@anticrm/presentation'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import contact from '../plugin'
export let integrations: Set<Ref<Doc>> | undefined = undefined
export let channels: Channel[] = []
export let channels: AttachedData<Channel>[] = []
const dispatch = createEventDispatcher()
</script>
{#if !channels.length}
{#if channels?.length === 0}
<CircleButton
icon={IconAdd}
size={'small'}

View File

@ -40,7 +40,7 @@
"@anticrm/text-editor": "~0.6.0",
"@anticrm/chunter": "~0.6.0",
"@anticrm/contact": "~0.6.2",
"@anticrm/login": "~0.6.0",
"@anticrm/login": "~0.6.1",
"deep-equal": "^2.0.5",
"@anticrm/panel": "~0.6.0",
"@anticrm/activity":"~0.6.0",
@ -52,6 +52,7 @@
"@anticrm/view-resources": "~0.6.0",
"@anticrm/task": "~0.6.0",
"@anticrm/task-resources": "~0.6.0",
"@anticrm/contact-resources": "~0.6.0"
"@anticrm/contact-resources": "~0.6.0",
"@anticrm/rekoni": "~0.6.0"
}
}

View File

@ -12,18 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import attachment from '@anticrm/attachment'
import contact, { Channel, combineName, Person } from '@anticrm/contact'
import type { Data, MixinData, Ref } from '@anticrm/core'
import contact, { Channel, ChannelProvider, combineName, Person } from '@anticrm/contact'
import { Channels } from '@anticrm/contact-resources'
import type { AttachedData, Data, MixinData, Ref } from '@anticrm/core'
import { generateId } from '@anticrm/core'
import { getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
import { EditableAvatar, Card, getClient, PDFViewer } from '@anticrm/presentation'
import login from '@anticrm/login'
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
import { Card, EditableAvatar, getClient, getFileUrl, PDFViewer } from '@anticrm/presentation'
import type { Candidate } from '@anticrm/recruit'
import { recognizeDocument } from '@anticrm/rekoni'
import { EditBox, IconFile as FileIcon, Label, Link, showPopup, Spinner } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { Channels } from '@anticrm/contact-resources'
import recruit from '../plugin'
import FileUpload from './icons/FileUpload.svelte'
import YesNo from './YesNo.svelte'
@ -51,9 +52,7 @@
async function createCandidate () {
const uploadFile = await getResource(attachment.helper.UploadFile)
const avatarProp = avatar !== undefined
? { avatar: await uploadFile(avatar) }
: {}
const avatarProp = avatar !== undefined ? { avatar: await uploadFile(avatar) } : {}
const candidate: Data<Person> = {
name: combineName(firstName, lastName),
city: object.city,
@ -66,24 +65,44 @@
}
const id = await client.createDoc(contact.class.Person, contact.space.Contacts, candidate, candidateId)
await client.createMixin(id as Ref<Person>, contact.class.Person, contact.space.Contacts, recruit.mixin.Candidate, candidateData)
await client.createMixin(
id as Ref<Person>,
contact.class.Person,
contact.space.Contacts,
recruit.mixin.Candidate,
candidateData
)
console.log('resume name', resume.name)
if (resume.uuid !== undefined) {
client.addCollection(attachment.class.Attachment, contact.space.Contacts, id, contact.class.Person, 'attachments', {
client.addCollection(
attachment.class.Attachment,
contact.space.Contacts,
id,
contact.class.Person,
'attachments',
{
name: resume.name,
file: resume.uuid,
size: resume.size,
type: resume.type,
lastModified: resume.lastModified
})
}
)
}
for (const channel of channels) {
await client.addCollection(contact.class.Channel, contact.space.Contacts, candidateId, contact.class.Person, 'channels', {
await client.addCollection(
contact.class.Channel,
contact.space.Contacts,
candidateId,
contact.class.Person,
'channels',
{
value: channel.value,
provider: channel.provider
})
}
)
}
dispatch('close')
@ -93,6 +112,71 @@
let loading = false
let dragover = false
function isUndef (value?: string): boolean {
return value === undefined || value === ''
}
function addChannel (channels: AttachedData<Channel>[], type: Ref<ChannelProvider>, value?: string): void {
if (value !== undefined) {
const provider = channels.find((e) => e.provider === type)
if (provider === undefined) {
channels.push({
provider: type,
value
})
} else {
if (isUndef(provider.value)) {
provider.value = value
}
}
}
}
async function recognize (name: string): Promise<void> {
const token = getMetadata(login.metadata.LoginToken) ?? ''
const fileUrl = window.location.origin + getFileUrl(resume.uuid)
try {
const doc = await recognizeDocument(token, fileUrl)
if (isUndef(firstName) && doc.firstName !== undefined) {
firstName = doc.firstName
}
if (isUndef(lastName) && doc.lastName !== undefined) {
lastName = doc.lastName
}
if (isUndef(object.city) && doc.city !== undefined) {
object.city = doc.city
}
if (isUndef(object.avatar) && doc.avatar !== undefined) {
// We had avatar, let's try to upload it.
const data = atob(doc.avatar)
let n = data.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = data.charCodeAt(n)
}
avatar = new File([u8arr], 'avatar.png', { type: 'image/png' })
}
const newChannels = [...channels]
addChannel(newChannels, contact.channelProvider.Email, doc.email)
addChannel(newChannels, contact.channelProvider.GitHub, doc.github)
addChannel(newChannels, contact.channelProvider.LinkedIn, doc.linkedin)
addChannel(newChannels, contact.channelProvider.Phone, doc.phone)
addChannel(newChannels, contact.channelProvider.Telegram, doc.telegram)
addChannel(newChannels, contact.channelProvider.Twitter, doc.twitter)
channels = newChannels
console.log(doc, channels)
} catch (err: any) {
console.error(err)
}
}
async function createAttachment (file: File) {
loading = true
try {
@ -107,6 +191,8 @@
resume.type = file.type
resume.lastModified = file.lastModified
recognize(resume.uuid)
console.log('uploaded file uuid', resume.uuid)
} catch (err: any) {
setPlatformStatus(unknownError(err))
@ -118,13 +204,17 @@
function drop (event: DragEvent) {
dragover = false
const droppedFile = event.dataTransfer?.files[0]
if (droppedFile !== undefined) { createAttachment(droppedFile) }
if (droppedFile !== undefined) {
createAttachment(droppedFile)
}
}
function fileSelected () {
console.log(inputFile.files)
const file = inputFile.files?.[0]
if (file !== undefined) { createAttachment(file) }
if (file !== undefined) {
createAttachment(file)
}
}
let avatar: File | undefined
@ -135,48 +225,77 @@
avatar = file
}
let channels: Channel[] = []
let channels: AttachedData<Channel>[] = []
</script>
<!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
<Card label={recruit.string.CreateCandidate}
<Card
label={recruit.string.CreateCandidate}
okAction={createCandidate}
canSave={firstName.length > 0 && lastName.length > 0}
space={contact.space.Contacts}
on:close={() => { dispatch('close') }}>
on:close={() => {
dispatch('close')
}}
>
<!-- <StatusComponent slot="error" status={{ severity: Severity.ERROR, code: 'Cant save the object because it already exists' }} /> -->
<div class="flex-row-center">
<div class="mr-4">
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone}/>
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
</div>
<div class="flex-col">
<div class="fs-title"><EditBox placeholder="John" maxWidth="10rem" bind:value={firstName}/></div>
<div class="fs-title mb-1"><EditBox placeholder="Appleseed" maxWidth="10rem" bind:value={lastName}/></div>
<div class="text-sm"><EditBox placeholder="Title" maxWidth="10rem" bind:value={object.title}/></div>
<div class="text-sm"><EditBox placeholder="Location" maxWidth="10rem" bind:value={object.city}/></div>
<div class="fs-title"><EditBox placeholder="John" maxWidth="10rem" bind:value={firstName} /></div>
<div class="fs-title mb-1"><EditBox placeholder="Appleseed" maxWidth="10rem" bind:value={lastName} /></div>
<div class="text-sm"><EditBox placeholder="Title" maxWidth="10rem" bind:value={object.title} /></div>
<div class="text-sm"><EditBox placeholder="Location" maxWidth="10rem" bind:value={object.city} /></div>
</div>
</div>
<div class="flex-row-center channels">
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
<Channels
bind:channels
on:change={(e) => {
channels = e.detail
}}
/>
</div>
<div class="flex-center resume" class:solid={dragover || resume.uuid}
on:dragover|preventDefault={ () => { dragover = true } }
on:dragleave={ () => { dragover = false } }
on:drop|preventDefault|stopPropagation={drop}>
<div
class="flex-center resume"
class:solid={dragover || resume.uuid}
on:dragover|preventDefault={() => {
dragover = true
}}
on:dragleave={() => {
dragover = false
}}
on:drop|preventDefault|stopPropagation={drop}
>
{#if resume.uuid}
<Link label={resume.name} href={'#'} icon={FileIcon} maxLenght={16} on:click={ () => { showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right') } }/>
<Link
label={resume.name}
href={'#'}
icon={FileIcon}
maxLenght={16}
on:click={() => {
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
}}
/>
{:else}
{#if loading}
<Link label={'Uploading...'} href={'#'} icon={Spinner} disabled />
{:else}
<Link label={'Add or drop resume'} href={'#'} icon={FileUpload} on:click={ () => { inputFile.click() } } />
<Link
label={'Add or drop resume'}
href={'#'}
icon={FileUpload}
on:click={() => {
inputFile.click()
}}
/>
{/if}
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected}/>
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected} />
{/if}
</div>
@ -195,9 +314,9 @@
.locations {
span {
margin-bottom: .125rem;
margin-bottom: 0.125rem;
font-weight: 500;
font-size: .75rem;
font-size: 0.75rem;
color: var(--theme-content-accent-color);
}
@ -205,7 +324,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-top: .75rem;
margin-top: 0.75rem;
color: var(--theme-caption-color);
}
}
@ -218,11 +337,13 @@
.resume {
margin-top: 1rem;
padding: .75rem;
padding: 0.75rem;
background: var(--theme-zone-bg);
border: 1px dashed var(--theme-zone-border);
border-radius: .5rem;
border-radius: 0.5rem;
backdrop-filter: blur(10px);
&.solid { border-style: solid; }
&.solid {
border-style: solid;
}
}
</style>

View File

@ -981,6 +981,10 @@
"projectFolder": "tests/sanity",
"shouldPublish": false
},
{
"packageName": "@anticrm/rekoni",
"projectFolder": "packages/rekoni",
"shouldPublish": true
},
]
}