Support for markup type (#936)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-07 16:21:32 +07:00 committed by GitHub
parent 53713d8c18
commit 666b4446b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 657 additions and 670 deletions

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,9 @@
"build": "cross-env NODE_ENV=production webpack --stats-error-details && echo 'done'",
"analyze": "cross-env NODE_ENV=production webpack --json > stats.json",
"show": "webpack-bundle-analyzer stats.json dist",
"dev": "cross-env CLIENT_TYPE=dev webpack serve --content-base public",
"dev-server": "cross-env CLIENT_TYPE=dev-server webpack serve --content-base public",
"start": "cross-env NODE_ENV=production webpack serve --content-base public",
"dev": "cross-env CLIENT_TYPE=dev webpack serve",
"dev-server": "cross-env CLIENT_TYPE=dev-server webpack serve",
"start": "cross-env NODE_ENV=production webpack serve",
"preformat-svelte": "prettier -w src/**/*.svelte",
"lint": "eslint --max-warnings=0 src",
"lint:fix": "yarn preformat-svelte && eslint --fix src",
@ -21,9 +21,9 @@
"mini-css-extract-plugin": "^2.2.0",
"dotenv-webpack": "^7.0.2",
"ts-loader": "^9.2.5",
"svelte-loader": "^3.1.0",
"svelte-loader": "^3.1.2",
"css-loader": "^5.2.1",
"webpack-dev-server": "^3.11.2",
"webpack-dev-server": "^4.7.4",
"style-loader": "^3.2.1",
"file-loader": "^6.2.0",
"sass-loader": "^12.1.0",

View File

@ -19,6 +19,7 @@ const path = require('path')
const autoprefixer = require('autoprefixer')
const CompressionPlugin = require('compression-webpack-plugin')
const DefinePlugin = require('webpack').DefinePlugin
const { resolve } = require('path')
const mode = process.env.NODE_ENV || 'development'
const prod = mode === 'production'
@ -29,7 +30,7 @@ module.exports = {
entry: {
bundle: [
'@anticrm/theme/styles/global.scss',
...(dev ? ['./src/main-dev.ts']: ['./src/main.ts'] )
...(dev ? ['./src/main-dev.ts']: ['./src/main.ts'] ),
]
},
resolve: {
@ -59,10 +60,37 @@ module.exports = {
loader: 'svelte-loader',
options: {
compilerOptions: {
dev: !prod,
dev: !prod
},
emitCss: true,
preprocess: require('svelte-preprocess')({ postcss: true })
hotReload: !prod,
preprocess: require('svelte-preprocess')({ postcss: true }),
hotOptions: {
// Prevent preserving local component state
preserveLocalState: true,
// If this string appears anywhere in your component's code, then local
// state won't be preserved, even when noPreserveState is false
noPreserveStateKey: '@!hmr',
// Prevent doing a full reload on next HMR update after fatal error
noReload: true,
// Try to recover after runtime errors in component init
optimistic: false,
// --- Advanced ---
// Prevent adding an HMR accept handler to components with
// accessors option to true, or to components with named exports
// (from <script context="module">). This have the effect of
// recreating the consumer of those components, instead of the
// component themselves, on HMR updates. This might be needed to
// reflect changes to accessors / named exports in the parents,
// depending on how you use them.
acceptAccessors: true,
acceptNamedExports: true,
}
}
}
},
@ -140,13 +168,24 @@ module.exports = {
new Dotenv({path: prod ? '.env-prod' : '.env'}),
new DefinePlugin({
'process.env.CLIENT_TYPE': JSON.stringify(process.env.CLIENT_TYPE)
})
})
],
devtool: prod ? false : 'source-map',
devtool: prod ? false : 'inline-source-map',
devServer: {
publicPath: '/',
static: {
directory: path.resolve(__dirname, "public"),
publicPath: "/",
serveIndex: true,
watch: true,
},
historyApiFallback: {
disableDotRule: true
},
hot: true,
client: {
logging: "info",
overlay: false,
progress: false,
},
proxy: devServer ? {
'/account': {

View File

@ -17,7 +17,7 @@ import activity from '@anticrm/activity'
import type { Backlink, Channel, Comment, Message } from '@anticrm/chunter'
import type { Class, Doc, Domain, Ref } from '@anticrm/core'
import { IndexKind } from '@anticrm/core'
import { Builder, Index, Model, Prop, TypeString, UX } from '@anticrm/model'
import { Builder, Index, Model, Prop, TypeMarkup, UX } from '@anticrm/model'
import core, { TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
@ -34,7 +34,7 @@ export class TChannel extends TSpace implements Channel {}
@Model(chunter.class.Message, core.class.Doc, DOMAIN_CHUNTER)
export class TMessage extends TDoc implements Message {
@Prop(TypeString(), 'Content' as IntlString)
@Prop(TypeMarkup(), 'Content' as IntlString)
@Index(IndexKind.FullText)
content!: string
}
@ -42,7 +42,7 @@ export class TMessage extends TDoc implements Message {
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@UX('Comment' as IntlString)
export class TComment extends TAttachedDoc implements Comment {
@Prop(TypeString(), 'Message' as IntlString)
@Prop(TypeMarkup(), 'Message' as IntlString)
@Index(IndexKind.FullText)
message!: string
}

View File

@ -49,13 +49,13 @@ export class TContact extends TDoc implements Contact {
avatar?: string
@Prop(Collection(contact.class.Channel), 'Contact Info' as IntlString)
@Prop(Collection(contact.class.Channel), contact.string.ContactInfo)
channels?: number
@Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString)
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
@Prop(Collection(chunter.class.Comment), 'Comments' as IntlString)
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
@Prop(TypeString(), 'Location' as IntlString)

View File

@ -48,7 +48,8 @@ export const ids = mergeIds(contactId, contact, {
CreateOrganizations: '' as IntlString,
SearchEmployee: '' as IntlString,
SearchPerson: '' as IntlString,
SearchOrganization: '' as IntlString
SearchOrganization: '' as IntlString,
ContactInfo: '' as IntlString
},
completion: {
PersonQuery: '' as Resource<ObjectSearchFactory>,

View File

@ -106,6 +106,9 @@ export class TType extends TObj implements Type<any> {
@Model(core.class.TypeString, core.class.Type)
export class TTypeString extends TType {}
@Model(core.class.TypeMarkup, core.class.Type)
export class TTypeMarkup extends TType {}
@Model(core.class.RefTo, core.class.Type)
export class TRefTo extends TType implements RefTo<Doc> {
to!: Ref<Class<Doc>>

View File

@ -30,6 +30,7 @@ import {
TTypeBoolean,
TTypeDate,
TTypeString,
TTypeMarkup,
TTypeTimestamp,
TVersion
} from './core'
@ -74,6 +75,7 @@ export function createModel (builder: Builder): void {
TAttribute,
TType,
TTypeString,
TTypeMarkup,
TTypeBoolean,
TTypeTimestamp,
TRefTo,

View File

@ -15,7 +15,7 @@
import type { Employee } from '@anticrm/contact'
import { Doc, FindOptions, Lookup, Ref, Timestamp } from '@anticrm/core'
import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model'
import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeMarkup, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact, { TPerson } from '@anticrm/model-contact'
@ -31,7 +31,7 @@ import presentation from '@anticrm/model-presentation'
@Model(recruit.class.Vacancy, task.class.SpaceWithStates)
@UX(recruit.string.Vacancy, recruit.icon.Vacancy)
export class TVacancy extends TSpaceWithStates implements Vacancy {
@Prop(TypeString(), 'Full description' as IntlString)
@Prop(TypeMarkup(), 'Full description' as IntlString)
fullDescription?: string
@Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString)
@ -71,19 +71,19 @@ export class TCandidate extends TPerson implements Candidate {
}
@Model(recruit.class.Applicant, task.class.Task)
@UX('Application' as IntlString, recruit.icon.Application, 'APP' as IntlString, 'number')
@UX(recruit.string.Application, recruit.icon.Application, 'APP' as IntlString, 'number')
export class TApplicant extends TTask implements Applicant {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.mixin.Candidate), 'Candidate' as IntlString)
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Candidate)
declare attachedTo: Ref<Candidate>
@Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString)
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
@Prop(Collection(chunter.class.Comment), 'Comments' as IntlString)
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
comments?: number
@Prop(TypeRef(contact.class.Employee), 'Assigned recruiter' as IntlString)
@Prop(TypeRef(contact.class.Employee), recruit.string.AssignedRecruiter)
declare assignee: Ref<Employee> | null
}

View File

@ -38,7 +38,9 @@ export default mergeIds(recruitId, recruit, {
RecruitApplication: '' as IntlString,
Vacancies: '' as IntlString,
CandidatePools: '' as IntlString,
SearchApplication: '' as IntlString
SearchApplication: '' as IntlString,
Application: '' as IntlString,
AssignedRecruiter: '' as IntlString
},
validator: {
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>

View File

@ -23,6 +23,7 @@ import {
Model,
Prop, TypeBoolean,
TypeDate,
TypeMarkup,
TypeRef,
TypeString,
UX
@ -131,7 +132,7 @@ export class TIssue extends TTask implements Issue {
@Prop(TypeString(), task.string.IssueName)
name!: string
@Prop(TypeString(), task.string.TaskDescription)
@Prop(TypeMarkup(), task.string.TaskDescription)
description!: string
@Prop(Collection(chunter.class.Comment), task.string.TaskComments)

View File

@ -118,6 +118,10 @@ export function createModel (builder: Builder): void {
presenter: view.component.StringPresenter
})
builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.HTMLPresenter
})
builder.mixin(core.class.TypeBoolean, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.BooleanPresenter
})

View File

@ -31,6 +31,7 @@ export default mergeIds(viewId, view, {
component: {
StringEditor: '' as AnyComponent,
StringPresenter: '' as AnyComponent,
HTMLPresenter: '' as AnyComponent,
BooleanPresenter: '' as AnyComponent,
BooleanEditor: '' as AnyComponent,
TimestampPresenter: '' as AnyComponent,

View File

@ -44,6 +44,7 @@ export default plugin(coreId, {
Space: '' as Ref<Class<Space>>,
Account: '' as Ref<Class<Account>>,
TypeString: '' as Ref<Class<Type<string>>>,
TypeMarkup: '' as Ref<Class<Type<string>>>,
TypeBoolean: '' as Ref<Class<Type<boolean>>>,
TypeTimestamp: '' as Ref<Class<Type<Timestamp>>>,
TypeDate: '' as Ref<Class<Type<Timestamp | Date>>>,

View File

@ -328,6 +328,13 @@ export function TypeString (): Type<string> {
return { _class: core.class.TypeString, label: 'TypeString' as IntlString }
}
/**
* @public
*/
export function TypeMarkup (): Type<string> {
return { _class: core.class.TypeMarkup, label: 'TypeMarkup' as IntlString }
}
/**
* @public
*/

View File

@ -1,6 +1,10 @@
{
"string": {
"Suggested": "SUGGESTED",
"NoItems": "No items"
"NoItems": "No items",
"EditorPlaceholder": "Start typing...",
"Edit": "Edit",
"Cancel": "Cancel",
"Save": "Save"
}
}

View File

@ -0,0 +1,90 @@
<script lang="ts">
import { IntlString } from '@anticrm/platform'
import { MessageViewer } from '@anticrm/presentation'
import { ActionIcon, IconCheck, IconClose, IconEdit } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import textEditorPlugin from '../plugin'
import StyledTextEditor from './StyledTextEditor.svelte'
export let content: string
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
let rawValue: string
const Mode = {
View: 1,
Edit: 2
}
let mode = Mode.View
let textEditor: StyledTextEditor
export function submit (): void {
textEditor.submit()
}
const dispatch = createEventDispatcher()
</script>
<div class="antiComponent styled-box">
{#if mode !== Mode.View}
<StyledTextEditor
{placeholder}
bind:content={rawValue}
bind:this={textEditor}
on:value={(evt) => {
rawValue = evt.detail
}}
>
<div class="flex flex-reverse flex-grow">
<div class="ml-2">
<!-- disabled={rawValue.trim().length === 0} -->
<ActionIcon
icon={IconCheck}
size={'medium'}
direction={'bottom'}
label={textEditorPlugin.string.Save}
action={() => {
dispatch('value', rawValue)
content = rawValue
mode = Mode.View
}}
/>
</div>
<ActionIcon
size={'medium'}
icon={IconClose}
direction={'top'}
label={textEditorPlugin.string.Cancel}
action={() => {
mode = Mode.View
}}
/>
</div>
</StyledTextEditor>
{:else}
<div class="text">
<MessageViewer message={content} />
</div>
<div class="flex flex-reverse">
<ActionIcon
size={'medium'}
icon={IconEdit}
direction={'top'}
label={textEditorPlugin.string.Edit}
action={() => {
rawValue = content ?? ''
mode = Mode.Edit
}}
/>
</div>
{/if}
</div>
<style lang="scss">
.styled-box {
flex-grow: 1;
}
.text {
flex-grow: 1;
line-height: 150%;
}
</style>

View File

@ -13,16 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { IntlString } from '@anticrm/platform'
import { ScrollBox } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import Emoji from './icons/Emoji.svelte'
import GIF from './icons/GIF.svelte'
import TextStyle from './icons/TextStyle.svelte'
import TextEditor from './TextEditor.svelte'
import textEditorPlugin from '../plugin'
const dispatch = createEventDispatcher()
export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
let textEditor: TextEditor
@ -37,6 +41,7 @@
<ScrollBox bothScroll stretch>
<TextEditor
bind:content
{placeholder}
bind:this={textEditor}
on:value
on:content={(ev) => {
@ -44,6 +49,7 @@
content = ''
textEditor.clear()
}}
on:blur
supportSubmit={false}
/>
</ScrollBox>

View File

@ -13,8 +13,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { IntlString, translate } from '@anticrm/platform'
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
@ -22,15 +23,22 @@
import Placeholder from '@tiptap/extension-placeholder'
import StarterKit from '@tiptap/starter-kit'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import textEditorPlugin from '../plugin'
export let content: string = ''
export let placeholder: string = 'Type something...'
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let extensions: AnyExtension[] = []
export let supportSubmit = true
let element: HTMLElement
let editor: Editor
let placeHolderStr: string = ''
$: ph = translate(placeholder, {}).then((r) => {
placeHolderStr = r
})
const dispatch = createEventDispatcher()
export function submit (): void {
@ -70,29 +78,31 @@
})
onMount(() => {
editor = new Editor({
element,
content: content,
extensions: [
StarterKit,
Highlight,
Link,
...(supportSubmit ? [Handle] : []), // order important
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
Placeholder.configure({ placeholder: placeholder }),
...extensions
],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
},
onBlur: () => {
dispatch('blur')
},
onUpdate: () => {
content = editor.getHTML()
dispatch('value', content)
}
ph.then(() => {
editor = new Editor({
element,
content: content,
extensions: [
StarterKit,
Highlight,
Link,
...(supportSubmit ? [Handle] : []), // order important
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
Placeholder.configure({ placeholder: placeHolderStr }),
...extensions
],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
},
onBlur: () => {
dispatch('blur', editor.getHTML())
},
onUpdate: () => {
content = editor.getHTML()
dispatch('value', content)
}
})
})
})
@ -103,35 +113,39 @@
})
</script>
<div style="width: 100%;" bind:this={element}/>
<div style="width: 100%;" bind:this={element} />
<style lang="scss" global>
.ProseMirror {
overflow-y: auto;
max-height: 5.5rem;
outline: none;
line-height: 150%;
p:not(:last-child) {
margin-block-end: 1em;
}
.ProseMirror {
overflow-y: auto;
max-height: 5.5rem;
outline: none;
line-height: 150%;
p:not(:last-child) {
margin-block-end: 1em;
> * + * {
margin-top: 0.75em;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--theme-content-trans-color);
pointer-events: none;
height: 0;
}
&::-webkit-scrollbar-thumb {
background-color: var(--theme-bg-accent-hover);
}
&::-webkit-scrollbar-corner {
background-color: var(--theme-bg-accent-hover);
}
&::-webkit-scrollbar-track {
margin: 0;
}
}
> * + * {
margin-top: 0.75em;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--theme-content-trans-color);
pointer-events: none;
height: 0;
}
&::-webkit-scrollbar-thumb { background-color: var(--theme-bg-accent-hover); }
&::-webkit-scrollbar-corner { background-color: var(--theme-bg-accent-hover); }
&::-webkit-scrollbar-track { margin: 0; }
}
</style>
</style>

View File

@ -23,9 +23,10 @@ export * from './types'
export { default as ReferenceInput } from './components/ReferenceInput.svelte'
export { default as TextEditor } from './components/TextEditor.svelte'
export { default as StyledTextEditor } from './components/StyledTextEditor.svelte'
export { default as StyledTextBox } from './components/StyledTextBox.svelte'
addStringsLoader(textEditorId, async (lang: string) => {
return await import(`../lang/${lang}.json`)
})
export { default } from './plugin'
export { default } from './plugin'

View File

@ -33,6 +33,10 @@ export default plugin(textEditorId, {
Attach: '' as IntlString,
TextStyle: '' as IntlString,
Emoji: '' as IntlString,
GIF: '' as IntlString
GIF: '' as IntlString,
EditorPlaceholder: '' as IntlString,
Edit: '' as IntlString,
Cancel: '' as IntlString,
Save: '' as IntlString
}
})

View File

@ -0,0 +1,9 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 16 16" fill='none' xmlns="http://www.w3.org/2000/svg">
<path d="M2 8.3324L6.65721 13L14 3" stroke={fill} stroke-linecap="round" stroke-linejoin="round"/>
</svg>

View File

@ -87,6 +87,7 @@ export { default as IconDelete } from './components/icons/Delete.svelte'
export { default as IconEdit } from './components/icons/Edit.svelte'
export { default as IconInfo } from './components/icons/Info.svelte'
export { default as IconBlueCheck } from './components/icons/BlueCheck.svelte'
export { default as IconCheck } from './components/icons/Check.svelte'
export { default as IconArrowLeft } from './components/icons/ArrowLeft.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'

View File

@ -18,7 +18,6 @@
import { CommentInput } from '@anticrm/chunter-resources'
import { Doc, SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { ReferenceInput } from '@anticrm/text-editor'
import { Grid, IconActivity, ScrollBox } from '@anticrm/ui'
import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity'
import TxView from './TxView.svelte'
@ -51,7 +50,7 @@
</script>
{#if fullSize || transparent}
{#if !transparent}
{#if transparent !== undefined && !transparent}
<div class="flex-row-center header">
<div class="flex-center icon"><IconActivity size={'small'} /></div>
<div class="fs-title">Activity</div>

View File

@ -18,7 +18,7 @@
import type { TxViewlet } from '@anticrm/activity'
import activity from '@anticrm/activity'
import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
import { Doc, Ref } from '@anticrm/core'
import core, { AnyAttribute, Doc, Ref } from '@anticrm/core'
import { Asset, getResource } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import {
@ -106,6 +106,11 @@
edit = false
props = { ...props, edit }
}
function isMessageType (attr?: AnyAttribute): boolean {
return attr?.type._class === core.class.TypeMarkup
}
$: hasMessageType = model.find(m => isMessageType(m.attribute))
</script>
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && tx.removed)) || model.length > 0}
<div class="flex-between msgactivity-container">
@ -153,14 +158,25 @@
{/if}
</div>
{/if}
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
{#each model as m}
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
{#each model as m, i}
{#await getValue(client, m, tx.updateTx.operations) then value}
{#if value === null}
<span>unset <Label label={m.label} /></span>
{:else}
<span>changed <Label label={m.label} /> to</span>
<div class="strong"><svelte:component this={m.presenter} {value} /></div>
<span class:flex-grow={hasMessageType}>changed <Label label={m.label} /> to</span>
{#if hasMessageType}
<div class="time"><TimeSince value={tx.tx.modifiedOn} /></div>
{/if}
{#if isMessageType(m.attribute)}
<div class="strong message emphasized">
<svelte:component this={m.presenter} {value} />
</div>
{:else}
<div class="strong">
<svelte:component this={m.presenter} {value} />
</div>
{/if}
{/if}
{/await}
{/each}
@ -171,7 +187,15 @@
<span>unset <Label label={m.label} /></span>
{:else}
<span>changed <Label label={m.label} /> to</span>
<div class="strong"><svelte:component this={m.presenter} {value} /></div>
{#if isMessageType(m.attribute)}
<div class="strong message emphasized">
<svelte:component this={m.presenter} {value} />
</div>
{:else}
<div class="strong">
<svelte:component this={m.presenter} {value} />
</div>
{/if}
{/if}
{/await}
{/each}
@ -183,7 +207,9 @@
{/if}
{/if}
</div>
<div class="time"><TimeSince value={tx.tx.modifiedOn} /></div>
{#if !hasMessageType}
<div class="time"><TimeSince value={tx.tx.modifiedOn} /></div>
{/if}
</div>
{#if viewlet && viewlet.component && viewlet.display !== 'inline'}
@ -282,4 +308,8 @@
border-radius: .75rem;
padding: 1rem 1.25rem;
}
.message {
flex-basis: 100%;
}
</style>

View File

@ -14,6 +14,7 @@
"EditUpdate": "Save...",
"EditCancel": "Cancel",
"Comments" : "Comments",
"MentionedIn": "mentioned this "
"MentionedIn": "mentioned this ",
"ContactInfo": "Contact Info"
}
}

View File

@ -4,11 +4,12 @@
"Vacancies": "Vacancies",
"CandidatePools": "Candidates pool",
"Candidates": "Candidates",
"VacancyName": "Vacancy Title *",
"VacancyName": "Vacancy Name *",
"VacancyDescription": "Vacancy Description",
"CreateVacancy": "Create Vacancy",
"MakePrivate": "Make Private",
"Vacancy": "Vacancy",
"VacancyPlaceholder": "Please type vacancy name",
"CandidatesName": "Pool name *",
"MakePrivateDescription": "Only members can see it",
"CreateAnApplication": "Create an application",
@ -31,7 +32,9 @@
"WorkLocationPreferences": "Work location preferences",
"Onsite": "Onsite",
"Remote": "Remote",
"SearchApplication": "Search for application..."
"SearchApplication": "Search for application...",
"Application": "Application",
"AssignedRecruiter": "Assigned recruiter"
},
"status": {
"CandidateRequired": "Please select candidate",

View File

@ -15,15 +15,15 @@
-->
<script lang="ts">
import type { IntlString } from '@anticrm/platform'
import type { Ref } from '@anticrm/core'
import { IconClose, Label, EditBox, ToggleWithLabel, Grid, Icon, Component } from '@anticrm/ui'
import { TextEditor } from '@anticrm/text-editor'
import { AttributesBar, getClient, createQuery } from '@anticrm/presentation'
import { Vacancy } from '@anticrm/recruit'
import { createEventDispatcher } from 'svelte'
import activity from '@anticrm/activity'
import { Attachments } from '@anticrm/attachment-resources'
import type { Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { AttributesBar, createQuery, getClient } from '@anticrm/presentation'
import { Vacancy } from '@anticrm/recruit'
import { StyledTextBox } from '@anticrm/text-editor'
import { Component, EditBox, Grid, Icon, IconClose, Label, ToggleWithLabel } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
export let _id: Ref<Vacancy>
@ -36,11 +36,17 @@
const query = createQuery()
const clazz = client.getHierarchy().getClass(recruit.class.Vacancy)
$: query.query(recruit.class.Vacancy, { _id }, result => { object = result[0] })
async function updateObject (_id: Ref<Vacancy>): Promise<void> {
await query.query(recruit.class.Vacancy, { _id }, result => {
object = result[0]
})
}
$: updateObject(_id)
const tabs: IntlString[] = ['General' as IntlString, 'Members' as IntlString, 'Activity' as IntlString]
let selected = 0
let textEditor: TextEditor
function onChange (key:string, value: any): void {
client.updateDoc(object._class, object.space, object._id, { [key]: value })
@ -81,13 +87,20 @@
<div class="flex-col box">
{#if selected === 0}
<Grid column={1} rowGap={1.5}>
<EditBox label={recruit.string.VacancyName} bind:value={object.name} placeholder="Software Engineer" maxWidth="39rem" focus on:change={() => {onChange('name', object.name)}}/>
<EditBox label={recruit.string.Description} bind:value={object.description} placeholder='Description' maxWidth="39rem" focus on:change={() => {onChange('description', object.description)}}/>
<EditBox label={recruit.string.VacancyName} bind:value={object.name} placeholder={recruit.string.VacancyPlaceholder} maxWidth="39rem" focus on:change={() => {
if (object.name.trim().length > 0) {
onChange('name', object.name)
} else {
// Revert previos object.name
updateObject(_id)
}
}}/>
<EditBox label={recruit.string.Description} bind:value={object.description} placeholder={recruit.string.VacancyDescription} maxWidth="39rem" focus on:change={() => { onChange('description', object.description) }}/>
</Grid>
<div class="mt-10">
<span class="title">Description</span>
<span class="title">Details</span>
<div class="description-container">
<TextEditor bind:this={textEditor} bind:content={object.fullDescription} on:blur={textEditor.submit} on:content={() => {onChange('fullDescription', object.fullDescription)}} />
<StyledTextBox bind:content={object.fullDescription} on:value={(evt) => { onChange('fullDescription', evt.detail) }} />
</div>
</div>
<div class="mt-14">
@ -96,7 +109,7 @@
{:else if selected === 1}
<ToggleWithLabel label={recruit.string.ThisVacancyIsPrivate} description={recruit.string.MakePrivateDescription}/>
{:else if selected === 2}
<Component is={activity.component.Activity} props={{object, transparent: true}} />
<Component is={activity.component.Activity} props={{ object, transparent: true }} />
{/if}
</div>
</div>
@ -205,7 +218,7 @@
display: flex;
justify-content: space-between;
overflow-y: auto;
height: 100px;
height: 15rem;
padding: 0px 16px;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);

View File

@ -27,6 +27,7 @@ export default mergeIds(recruitId, recruit, {
string: {
CreateVacancy: '' as IntlString,
VacancyName: '' as IntlString,
VacancyPlaceholder: '' as IntlString,
VacancyDescription: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString,

View File

@ -17,6 +17,7 @@
import { getClient } from '@anticrm/presentation'
import type { Issue } from '@anticrm/task'
import task from '@anticrm/task'
import { StyledTextBox } from '@anticrm/text-editor'
import { EditBox, Grid } from '@anticrm/ui'
import { createEventDispatcher, onMount } from 'svelte'
import plugin from '../plugin'
@ -54,13 +55,24 @@
focus
on:change={() => change('name', object.name)}
/>
<EditBox
label={plugin.string.TaskDescription}
bind:value={object.description}
icon={task.icon.Task}
placeholder={plugin.string.TaskDescriptionPlaceholder}
maxWidth="39rem"
on:change={() => change('description', object.description)}
/>
<div class='description'>
<StyledTextBox
bind:content={object.description}
placeholder={plugin.string.TaskDescriptionPlaceholder}
on:blur={() => change('description', object.description)}
/>
</div>
</Grid>
{/if}
<style lang="scss">
.description {
display: flex;
padding: 1rem;
height: 12rem;
border-radius: .25rem;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
}
</style>

View File

@ -50,7 +50,6 @@
if (item.dueTo !== dueTo) {
ops.dueTo = (dueTo?.getTime() ?? null) as unknown as Timestamp
}
console.log('AHTUNG', ops)
if (Object.keys(ops).length === 0) {
return

View File

@ -115,7 +115,7 @@
}}>
<div class="flex flex-reverse flex-grow">
<div class="ml-2">
<Button disabled={newTemplate.title.trim().length == 0 } primary label={templatesPlugin.string.SaveTemplate} on:click={saveNewTemplate} />
<Button disabled={newTemplate.title.trim().length === 0 } primary label={templatesPlugin.string.SaveTemplate} on:click={saveNewTemplate} />
</div>
<Button
label={templatesPlugin.string.Cancel}

View File

@ -0,0 +1,23 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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'
export let value: string
</script>
<MessageViewer message={value}/>

View File

@ -31,6 +31,7 @@ import MoveView from './components/Move.svelte'
import EditDoc from './components/EditDoc.svelte'
import RolePresenter from './components/RolePresenter.svelte'
import ObjectPresenter from './components/ObjectPresenter.svelte'
import HTMLPresenter from './components/HTMLPresenter.svelte'
export { default as ContextMenu } from './components/Menu.svelte'
export { buildModel, getActions, getObjectPresenter, LoadingProps } from './utils'
@ -72,6 +73,7 @@ export default async (): Promise<Resources> => ({
DatePresenter,
RolePresenter,
ObjectPresenter,
EditDoc
EditDoc,
HTMLPresenter
}
})

View File

@ -101,7 +101,8 @@ async function getAttributePresenter (
_class: attrClass,
label: preserveKey.label ?? attribute.label,
presenter,
icon: presenterMixin.icon
icon: presenterMixin.icon,
attribute
}
}

View File

@ -14,7 +14,7 @@
// limitations under the License.
//
import type { Class, Client, Doc, DocumentQuery, FindOptions, Mixin, Obj, Ref, Space, TxOperations, UXObject } from '@anticrm/core'
import type { AnyAttribute, Class, Client, Doc, DocumentQuery, FindOptions, Mixin, Obj, Ref, Space, TxOperations, UXObject } from '@anticrm/core'
import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
@ -116,6 +116,8 @@ export interface AttributeModel {
sortingKey: string
// Extra icon if applicable
icon?: Asset
attribute?: AnyAttribute
}
/**

View File

@ -14,18 +14,18 @@
-->
<script lang="ts">
import type { IntlString, Asset } from '@anticrm/platform'
import { Icon, Label } from '@anticrm/ui'
import type { Asset } from '@anticrm/platform'
import { Icon } from '@anticrm/ui'
export let icon: Asset | undefined
export let label: IntlString
export let description: IntlString | undefined
export let label: string
export let description: string | undefined
</script>
<div class="ac-header__wrap-description">
<div class="ac-header__wrap-title">
{#if icon }<div class="ac-header__icon"><Icon {icon} size={'small'}/></div>{/if}
<span class="ac-header__title"><Label {label}/></span>
<span class="ac-header__title">{label}</span>
</div>
{#if description }<span class="ac-header__description">{description}</span>{/if}
</div>

View File

@ -15,9 +15,9 @@
-->
<script lang="ts">
import type { Ref, Class, Doc, Space, WithLookup } from '@anticrm/core'
import type { Viewlet } from '@anticrm/view'
import type { Class, Doc, Ref, Space, WithLookup } from '@anticrm/core'
import { Component } from '@anticrm/ui'
import type { Viewlet } from '@anticrm/view'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space>
@ -30,7 +30,7 @@
<Component is={viewlet.$lookup?.descriptor?.component} props={ {
_class,
space,
options: viewlet.options,
options: viewlet.options,
config: viewlet.config,
search
} } />

View File

@ -19,7 +19,7 @@
import core from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { EditBox, Grid, Icon, IconClose, Label, ToggleWithLabel } from '@anticrm/ui'
import { EditBox, Grid, Icon, IconClose, Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
export let _id: Ref<Space>
@ -42,7 +42,13 @@
let selected = 0
function onNameChange (ev: Event) {
client.updateDoc(spaceClass, space.space, space._id, { name: (ev.target as HTMLInputElement).value })
const value = (ev.target as HTMLInputElement).value
if (value.trim().length > 0) {
client.updateDoc(spaceClass, space.space, space._id, { name: value })
} else {
// Just refresh value
query.query(core.class.Space, { _id }, result => { space = result[0] })
}
}
</script>
@ -71,9 +77,9 @@
{#if selected === 0}
{#if space}
<Grid column={1} rowGap={1.5}>
<EditBox label={clazz.label} icon={clazz.icon} bind:value={space.name} placeholder="Software Engineer" maxWidth="39rem" focus on:change={onNameChange}/>
<EditBox label={clazz.label} icon={clazz.icon} bind:value={space.name} placeholder={clazz.label} maxWidth="39rem" focus on:change={onNameChange}/>
<!-- <AttributeBarEditor maxWidth="39rem" object={space} key="name"/> -->
<ToggleWithLabel label={'MakePrivate'} description={'MakePrivateDescription'}/>
<!-- <ToggleWithLabel label={workbench.string.MakePrivate} description={workbench.string.MakePrivateDescription}/> -->
</Grid>
{/if}
{:else}

View File

@ -17,12 +17,12 @@
import core from '@anticrm/core'
import { getResource, IntlString } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import { Action, IconAdd, IconEdit, showPopup } from '@anticrm/ui'
import { Action, IconAdd, IconEdit, showPanel, showPopup } from '@anticrm/ui'
import { getActions as getContributedActions } from '@anticrm/view-resources'
import type { SpacesNavModel } from '@anticrm/workbench'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { classIcon } from '../../utils'
import SpacePanel from './SpacePanel.svelte'
import TreeItem from './TreeItem.svelte'
import TreeNode from './TreeNode.svelte'
@ -44,7 +44,7 @@
label: 'Open' as IntlString,
icon: IconEdit,
action: async (_id: Ref<Doc>): Promise<void> => {
showPopup(model.component ?? SpacePanel, { _id, spaceClass: model.spaceClass }, 'right')
showPanel(model.component ?? plugin.component.SpacePanel, _id, model.spaceClass, 'right')
}
}

View File

@ -18,6 +18,7 @@ import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import { Resources } from '@anticrm/platform'
import Archive from './components/Archive.svelte'
import { Space } from '@anticrm/core'
import SpacePanel from './components/navigator/SpacePanel.svelte'
function hasArchiveSpaces (spaces: Space[]): boolean {
return spaces.find(sp => sp.archived) !== undefined
@ -27,7 +28,8 @@ export default async (): Promise<Resources> => ({
component: {
WorkbenchApp,
ApplicationPresenter,
Archive
Archive,
SpacePanel
},
function: {
HasArchiveSpaces: hasArchiveSpaces

View File

@ -1,14 +1,14 @@
//
// Copyright © 2020 Anticrm Platform Contributors.
//
//
// 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.
//
@ -17,6 +17,7 @@ import { mergeIds } from '@anticrm/platform'
import type { IntlString } from '@anticrm/platform'
import workbench, { workbenchId } from '@anticrm/workbench'
import { AnyComponent } from '@anticrm/ui'
export default mergeIds(workbenchId, workbench, {
string: {
@ -27,5 +28,8 @@ export default mergeIds(workbenchId, workbench, {
HideMenu: '' as IntlString,
Archive: '' as IntlString,
Archived: '' as IntlString
},
component: {
SpacePanel: '' as AnyComponent
}
})

View File

@ -153,7 +153,7 @@ export class FullTextIndex implements WithFind {
const allAttributes = this.hierarchy.getAllAttributes(clazz)
const result: AnyAttribute[] = []
for (const [, attr] of allAttributes) {
if (attr.type._class === core.class.TypeString) {
if (attr.type._class === core.class.TypeString || attr.type._class === core.class.TypeMarkup) {
result.push(attr)
}
}