Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-09-30 00:56:31 +06:00 committed by GitHub
parent 5f0ba95cee
commit 0aafafb93e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 415 additions and 52 deletions

View File

@ -20,7 +20,8 @@ import {
Event,
ReccuringEvent,
ReccuringInstance,
RecurringRule
RecurringRule,
Visibility
} from '@hcengineering/calendar'
import { Contact } from '@hcengineering/contact'
import { DateRangeMode, Domain, IndexKind, Markup, Ref, Timestamp } from '@hcengineering/core'
@ -60,7 +61,7 @@ export const DOMAIN_CALENDAR = 'calendar' as Domain
@Model(calendar.class.Calendar, core.class.Space)
@UX(calendar.string.Calendar, calendar.icon.Calendar)
export class TCalendar extends TSpaceWithStates implements Calendar {
visibility!: 'public' | 'freeBusy' | 'private'
visibility!: Visibility
sync?: boolean
}
@ -108,7 +109,7 @@ export class TEvent extends TAttachedDoc implements Event {
access!: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
visibility?: 'public' | 'freeBusy' | 'private'
visibility?: Visibility
}
@Model(calendar.class.ReccuringEvent, calendar.class.Event)
@ -117,6 +118,7 @@ export class TReccuringEvent extends TEvent implements ReccuringEvent {
rules!: RecurringRule[]
exdate!: Timestamp[]
rdate!: Timestamp[]
originalStartTime!: Timestamp
}
@Model(calendar.class.ReccuringInstance, calendar.class.Event)

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { Calendar, Event } from '@hcengineering/calendar'
import { Calendar, Event, ReccuringEvent } from '@hcengineering/calendar'
import core, { Ref, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import calendar from './plugin'
@ -76,10 +76,21 @@ async function migrateReminders (client: MigrationClient): Promise<void> {
}
}
async function fillOriginalStartTime (client: MigrationClient): Promise<void> {
const events = await client.find<ReccuringEvent>(DOMAIN_CALENDAR, {
_class: calendar.class.ReccuringEvent,
originalStartTime: { $exists: false }
})
for (const event of events) {
await client.update(DOMAIN_CALENDAR, { _id: event._id }, { originalStartTime: event.date })
}
}
export const calendarOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await fixEventDueDate(client)
await migrateReminders(client)
await fillOriginalStartTime(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)

View File

@ -37,7 +37,6 @@ export default mergeIds(tagsId, tags, {
ColorLabel: '' as IntlString,
WeightLabel: '' as IntlString,
TagReferenceLabel: '' as IntlString,
TagLabel: '' as IntlString,
TargetClassLabel: '' as IntlString,
TargetCategoryLabel: '' as IntlString,
TagReference: '' as IntlString,

View File

@ -38,6 +38,7 @@
export let autoSelect = true
export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = undefined
export let defaultIcon: AnySvelteComponent | Asset | ComponentType | undefined = undefined
export let readonly: boolean = false
const dispatch = createEventDispatcher()
@ -61,6 +62,7 @@
{component}
{componentProps}
{autoSelect}
{readonly}
{iconWithEmoji}
{defaultIcon}
bind:value={space}

View File

@ -17,7 +17,7 @@
export let checked: boolean = false
export let symbol: 'check' | 'minus' = 'check'
export let size: 'small' | 'medium' = 'small'
export let size: 'small' | 'medium' | 'large' = 'small'
export let circle: boolean = false
export let accented: boolean = false
export let readonly = false
@ -66,6 +66,10 @@
width: 1rem;
height: 1rem;
}
&.large {
width: 1.25rem;
height: 1.25rem;
}
&.circle {
width: 1rem;
height: 1rem;

View File

@ -27,6 +27,7 @@
export let placeholder: IntlString
export let items: ListItem[] = []
export let selected: ListItem | undefined = undefined
export let disabled: boolean = false
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
@ -35,6 +36,8 @@
export let labelDirection: TooltipAlignment | undefined = undefined
export let focusIndex = -1
export let withSearch: boolean = true
let container: HTMLElement
let opened: boolean = false
@ -45,16 +48,18 @@
<div bind:this={container} class="min-w-0">
<Button
{focusIndex}
{icon}
icon={selected?.icon ?? icon}
iconProps={selected?.iconProps}
width={width ?? 'min-content'}
{size}
{kind}
{justify}
{disabled}
showTooltip={{ label, direction: labelDirection }}
on:click={() => {
if (!opened) {
opened = true
showPopup(DropdownPopup, { title: label, items, icon }, container, (result) => {
showPopup(DropdownPopup, { title: label, items, icon, withSearch }, container, (result) => {
if (result) {
selected = result
dispatch('selected', result)

View File

@ -26,6 +26,7 @@
export let icon: Asset | AnySvelteComponent
export let placeholder: IntlString = plugin.string.SearchDots
export let items: ListItem[]
export let withSearch: boolean = true
let search: string = ''
let phTraslate: string = ''
@ -74,16 +75,18 @@
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')} on:keydown={onKeydown}>
<div class="header">
<input
bind:this={searchInput}
type="text"
bind:value={search}
placeholder={phTraslate}
on:input={(ev) => {}}
on:change
/>
</div>
{#if withSearch}
<div class="header">
<input
bind:this={searchInput}
type="text"
bind:value={search}
placeholder={phTraslate}
on:input={(ev) => {}}
on:change
/>
</div>
{/if}
<div class="scroll">
<div class="box">
<ListView bind:this={list} count={objects.length} bind:selection>
@ -97,10 +100,12 @@
handleSelection(evt, idx)
}}
>
{#if item.image || icon}
{#if item.image || item.icon || icon}
<div class="flex-center img" class:image={item.image}>
{#if item.image}
<img src={item.image} alt={item.label} />
{:else if item.icon}
<Icon icon={item.icon} size={'medium'} iconProps={item.iconProps} />
{:else if typeof icon === 'string'}
<Icon {icon} size={'small'} />
{:else}
@ -119,17 +124,17 @@
<style lang="scss">
.img {
margin-right: 0.75rem;
flex-shrink: 0;
width: 1.5rem;
height: 1.5rem;
flex-shrink: 0;
}
.image {
border-color: transparent;
color: var(--caption-color);
background-color: var(--popup-bg-hover);
border-radius: 50%;
outline: none;
overflow: hidden;
}
.image {
border-color: transparent;
img {
max-width: fit-content;
}

View File

@ -0,0 +1,13 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" {fill}>
<path
d="M8 2C11.3 2 14 4.7 14 8C14 11.3 11.3 14 8 14C4.7 14 2 11.3 2 8C2 4.7 4.7 2 8 2ZM8 1C4.15 1 1 4.15 1 8C1 11.85 4.15 15 8 15C11.85 15 15 11.85 15 8C15 4.15 11.85 1 8 1Z"
/>
<path
d="M12 8C12 7.72386 11.7761 7.5 11.5 7.5L8.5 7.5V4.5C8.5 4.22386 8.27614 4 8 4C7.72386 4 7.5 4.22386 7.5 4.5V7.5H4.5C4.22386 7.5 4 7.72386 4 8C4 8.27614 4.22386 8.5 4.5 8.5H7.5L7.5 11.5C7.5 11.7761 7.72386 12 8 12C8.27614 12 8.5 11.7761 8.5 11.5V8.5H11.5C11.7761 8.5 12 8.27614 12 8Z"
/>
</svg>

View File

@ -101,6 +101,7 @@ export { default as DropdownPopup } from './components/DropdownPopup.svelte'
export { default as DropdownLabels } from './components/DropdownLabels.svelte'
export { default as DropdownLabelsPopup } from './components/DropdownLabelsPopup.svelte'
export { default as DropdownLabelsIntl } from './components/DropdownLabelsIntl.svelte'
export { default as DropdownLabelsPopupIntl } from './components/DropdownLabelsPopupIntl.svelte'
export { default as DropdownRecord } from './components/DropdownRecord.svelte'
export { default as ShowMore } from './components/ShowMore.svelte'
export { default as Menu } from './components/Menu.svelte'
@ -115,6 +116,7 @@ export { default as Timeline } from './components/Timeline.svelte'
export { default as TimeShiftPresenter } from './components/TimeShiftPresenter.svelte'
export { default as IconAdd } from './components/icons/Add.svelte'
export { default as IconCircleAdd } from './components/icons/CircleAdd.svelte'
export { default as IconCopy } from './components/icons/Copy.svelte'
export { default as IconStart } from './components/icons/Start.svelte'
export { default as IconStop } from './components/icons/Stop.svelte'

View File

@ -280,6 +280,8 @@ export interface LabelAndProps {
export interface ListItem {
_id: string
label: string
icon?: Asset
iconProps?: any
image?: string
isSelectable?: boolean
fontWeight?: 'normal' | 'medium' | 'semi-bold'

View File

@ -37,4 +37,11 @@
<symbol id="globe" viewBox="0 0 32 32">
<path d="M16 2.00024C13.2311 2.00024 10.5243 2.82133 8.22202 4.35967C5.91973 5.89801 4.12532 8.08451 3.06569 10.6427C2.00607 13.2008 1.72882 16.0158 2.26901 18.7315C2.80921 21.4472 4.14258 23.9418 6.10051 25.8997C8.05845 27.8577 10.553 29.191 13.2687 29.7312C15.9845 30.2714 18.7994 29.9942 21.3576 28.9346C23.9157 27.8749 26.1022 26.0805 27.6406 23.7782C29.1789 21.4759 30 18.7692 30 16.0002C30 12.2872 28.525 8.72626 25.8995 6.10075C23.274 3.47524 19.713 2.00024 16 2.00024ZM28 15.0002H22C21.8833 11.3173 20.9291 7.70939 19.21 4.45024C21.5786 5.09814 23.6914 6.45709 25.2632 8.34367C26.8351 10.2302 27.7903 12.5536 28 15.0002ZM16 28.0002C15.7769 28.0152 15.5531 28.0152 15.33 28.0002C13.2583 24.6964 12.1085 20.8984 12 17.0002H20C19.9005 20.8956 18.7612 24.6934 16.7 28.0002C16.467 28.0166 16.2331 28.0166 16 28.0002ZM12 15.0002C12.0995 11.1049 13.2388 7.30707 15.3 4.00024C15.7453 3.95021 16.1947 3.95021 16.64 4.00024C18.7223 7.30104 19.8825 11.0993 20 15.0002H12ZM12.76 4.45024C11.0513 7.71189 10.1075 11.3197 10 15.0002H4.00001C4.20971 12.5536 5.16495 10.2302 6.7368 8.34367C8.30865 6.45709 10.4214 5.09814 12.79 4.45024H12.76ZM4.05001 17.0002H10.05C10.1544 20.68 11.0948 24.2878 12.8 27.5502C10.4389 26.8954 8.33478 25.5334 6.77056 23.6474C5.20634 21.7614 4.25695 19.4418 4.05001 17.0002ZM19.21 27.5502C20.9291 24.2911 21.8833 20.6832 22 17.0002H28C27.7903 19.4469 26.8351 21.7702 25.2632 23.6568C23.6914 25.5434 21.5786 26.9023 19.21 27.5502Z" />
</symbol>
<symbol id="private" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.99999C6.89543 1.99999 6 2.89542 6 3.99998V6.99995H10V3.99998C10 2.89542 9.10457 1.99999 8 1.99999ZM11 6.99995V3.99998C11 2.34314 9.65685 1 8 1C6.34315 1 5 2.34314 5 3.99998V6.99995C3.89543 6.99995 3 7.89538 3 8.99994V12.9999C3 14.1045 3.89543 14.9999 5 14.9999H11C12.1046 14.9999 13 14.1045 13 12.9999V8.99994C13 7.89538 12.1046 6.99995 11 6.99995ZM5 7.99995C4.44772 7.99995 4 8.44766 4 8.99994V12.9999C4 13.5522 4.44772 13.9999 5 13.9999H11C11.5523 13.9999 12 13.5522 12 12.9999V8.99994C12 8.44766 11.5523 7.99995 11 7.99995H5Z" fill="currentColor"/>
</symbol>
<symbol id="public" viewBox="0 0 16 16" fill="none">
<path d="M15.4698 7.83C14.8817 6.30882 13.8608 4.99331 12.5332 4.04604C11.2056 3.09878 9.62953 2.56129 7.99979 2.5C6.37005 2.56129 4.79398 3.09878 3.46639 4.04604C2.1388 4.99331 1.11787 6.30882 0.529787 7.83C0.490071 7.93985 0.490071 8.06015 0.529787 8.17C1.11787 9.69118 2.1388 11.0067 3.46639 11.954C4.79398 12.9012 6.37005 13.4387 7.99979 13.5C9.62953 13.4387 11.2056 12.9012 12.5332 11.954C13.8608 11.0067 14.8817 9.69118 15.4698 8.17C15.5095 8.06015 15.5095 7.93985 15.4698 7.83ZM7.99979 12.5C5.34979 12.5 2.54979 10.535 1.53479 8C2.54979 5.465 5.34979 3.5 7.99979 3.5C10.6498 3.5 13.4498 5.465 14.4648 8C13.4498 10.535 10.6498 12.5 7.99979 12.5Z" fill="currentColor"/>
<path d="M7.99979 5C7.40644 5 6.82642 5.17595 6.33308 5.50559C5.83973 5.83524 5.45521 6.30377 5.22815 6.85195C5.00109 7.40013 4.94168 8.00333 5.05743 8.58527C5.17319 9.16721 5.45891 9.70176 5.87847 10.1213C6.29802 10.5409 6.83257 10.8266 7.41452 10.9424C7.99646 11.0581 8.59966 10.9987 9.14784 10.7716C9.69602 10.5446 10.1646 10.1601 10.4942 9.66671C10.8238 9.17336 10.9998 8.59334 10.9998 8C10.9998 7.20435 10.6837 6.44129 10.1211 5.87868C9.5585 5.31607 8.79544 5 7.99979 5ZM7.99979 10C7.60422 10 7.21755 9.8827 6.88865 9.66294C6.55975 9.44318 6.3034 9.13082 6.15203 8.76537C6.00065 8.39991 5.96105 7.99778 6.03822 7.60982C6.11539 7.22186 6.30587 6.86549 6.58557 6.58579C6.86528 6.30608 7.22164 6.1156 7.60961 6.03843C7.99757 5.96126 8.3997 6.00087 8.76515 6.15224C9.13061 6.30362 9.44296 6.55996 9.66273 6.88886C9.88249 7.21776 9.99979 7.60444 9.99979 8C9.99979 8.53043 9.78907 9.03914 9.414 9.41421C9.03893 9.78929 8.53022 10 7.99979 10Z" fill="currentColor"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -78,6 +78,10 @@
"Busy": "Busy",
"AddReminder": "Add reminder",
"SeeAllNumberParticipants": "{value, plural, other {See all # participants}}",
"SeeAllNumberReminders": "{value, plural, other {See all # reminders}}"
"SeeAllNumberReminders": "{value, plural, other {See all # reminders}}",
"Visibility": "Visibility",
"Private": "Only visible to you",
"Public": "Visible to everyone",
"FreeBusy": "FreeBusy"
}
}

View File

@ -78,6 +78,10 @@
"Busy": "Занято",
"AddReminder": "Добавить напоминание",
"SeeAllNumberParticipants": "{value, plural, other {Увидеть всех # участников}}",
"SeeAllNumberReminders": "{value, plural, other {Просмотреть все # напоминаний}}"
"SeeAllNumberReminders": "{value, plural, other {Просмотреть все # напоминаний}}",
"Visibility": "Видимость",
"Private": "Видно только Вам",
"Public": "Видно всем",
"FreeBusy": "Скрыть детали"
}
}

View File

@ -26,7 +26,9 @@ loadMetadata(calendar.icon, {
Description: `${icons}#description`,
Participants: `${icons}#participants`,
Repeat: `${icons}#repeat`,
Globe: `${icons}#globe`
Globe: `${icons}#globe`,
Private: `${icons}#private`,
Public: `${icons}#public`
})
addStringsLoader(calendarId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -82,7 +82,8 @@
participants,
title,
allDay,
access: 'owner'
access: 'owner',
originalStartTime: allDay ? saveUTC(date) : date
})
} else {
await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'events', {

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Event, ReccuringInstance } from '@hcengineering/calendar'
import { Timestamp, Ref, DocumentUpdate } from '@hcengineering/core'
import { DocumentUpdate, Ref, Timestamp } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import ui, {
ActionIcon,
@ -29,16 +29,16 @@
closeTooltip,
deviceOptionsStore as deviceInfo,
day as getDay,
getEventPositionElement,
getMonday,
getWeekDayName,
resizeObserver,
showPopup,
getEventPositionElement
showPopup
} from '@hcengineering/ui'
import { Menu } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import calendar from '../plugin'
import { updateReccuringInstance, isReadOnly } from '../utils'
import { isReadOnly, updateReccuringInstance } from '../utils'
import EventElement from './EventElement.svelte'
export let events: Event[]
@ -563,7 +563,11 @@
if (originDueDate !== event.dueDate) update.dueDate = event.dueDate
if (Object.keys(update).length > 0) {
if (event._class === calendar.class.ReccuringInstance) {
await updateReccuringInstance(update, event as ReccuringInstance)
await updateReccuringInstance(update, {
...event,
date: originDate,
dueDate: originDueDate
} as ReccuringInstance)
} else {
await client.update(event, update)
}

View File

@ -16,6 +16,7 @@
import { Icon, areDatesEqual, IconArrowRight } from '@hcengineering/ui'
import calendar from '../plugin'
import DateEditor from './DateEditor.svelte'
import { createEventDispatcher } from 'svelte'
export let startDate: number
export let dueDate: number
@ -26,10 +27,12 @@
let diff = dueDate - startDate
const allDayDuration = 24 * 60 * 60 * 1000 - 1
const dispatch = createEventDispatcher()
function dateChange () {
startDate = allDay ? new Date(startDate).setHours(0, 0, 0, 0) : startDate
dueDate = startDate + (allDay ? allDayDuration : diff)
dispatch('change', { startDate, dueDate })
}
function dueChange () {
@ -40,6 +43,7 @@
dueDate = startDate + (allDay ? allDayDuration : diff)
}
diff = dueDate - startDate
dispatch('dueChange', { dueDate })
}
</script>

View File

@ -111,7 +111,6 @@
<div class="repeatPopup-container">
<div class="header">
<Label label={calendar.string.Repeat} />
{selected}
</div>
<div class="content flex-col">
<div class="flex-row-center gap-1-5">

View File

@ -159,6 +159,14 @@ export async function updateReccuringInstance (
eventId: object.recurringEventId
})
if (base !== undefined) {
if (ops.date !== undefined) {
const diff = object.date - ops.date
ops.date = base.date - diff
}
if (ops.dueDate !== undefined) {
const diff = object.dueDate - ops.dueDate
ops.dueDate = base.dueDate - diff
}
await client.update(base, ops)
}
} else if (res.mode === 'next') {

View File

@ -19,12 +19,19 @@ import { plugin } from '@hcengineering/platform'
import type { Handler, IntegrationType } from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
/**
* @public
*/
export type Visibility = 'public' | 'freeBusy' | 'private'
/**
* @public
*/
export interface Calendar extends Space {
visibility: 'public' | 'freeBusy' | 'private'
visibility: Visibility
sync?: boolean
externalId?: string
externalUser?: string
}
/**
@ -55,6 +62,7 @@ export interface ReccuringEvent extends Event {
rules: RecurringRule[]
exdate: Timestamp[]
rdate: Timestamp[]
originalStartTime: Timestamp
}
/**
@ -84,7 +92,7 @@ export interface Event extends AttachedDoc {
reminders?: Timestamp[]
visibility?: 'public' | 'freeBusy' | 'private'
visibility?: Visibility
access: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
}
@ -134,7 +142,9 @@ const calendarPlugin = plugin(calendarId, {
Description: '' as Asset,
Participants: '' as Asset,
Repeat: '' as Asset,
Globe: '' as Asset
Globe: '' as Asset,
Public: '' as Asset,
Private: '' as Asset
},
space: {
// deprecated
@ -166,7 +176,11 @@ const calendarPlugin = plugin(calendarId, {
PersonsLabel: '' as IntlString,
EventNumber: '' as IntlString,
Reminders: '' as IntlString,
Today: '' as IntlString
Today: '' as IntlString,
Visibility: '' as IntlString,
Public: '' as IntlString,
FreeBusy: '' as IntlString,
Private: '' as IntlString
},
handler: {
DisconnectHandler: '' as Handler

View File

@ -285,7 +285,7 @@ function getReccuringEventInstances (
const excludes = new Set(event.exdate ?? [])
res = res.filter((p) => !excludes.has(p.date))
res = res.filter((i) => {
const override = instances.find((p) => p.originalStartTime === i.date)
const override = instances.find((p) => p.originalStartTime === i.originalStartTime)
return override === undefined
})
return res

View File

@ -11,6 +11,7 @@
"AddTagTooltip": "Add/Create {word}",
"AddNowTooltip": "Create {word}",
"AddTag": "Create {word}",
"AddLabel": "Add label",
"EditTag": "Edit {word}",
"TagCreateLabel": "Tag",
"CancelLabel": "Cancel",

View File

@ -11,6 +11,7 @@
"AddTagTooltip": "Добавить/создать {word}",
"AddNowTooltip": "Создать {word}",
"AddTag": "Создать {word}",
"AddLabel": "Добавить метку",
"EditTag": "Редактировать {word}",
"TagCreateLabel": "Тег",
"CancelLabel": "Отмена",

View File

@ -0,0 +1,74 @@
<!--
// Copyright © 2023 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 { Class, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags, { TagReference } from '@hcengineering/tags'
import { Button, ButtonKind, Icon, Label, getEventPopupPositionElement, showPopup } from '@hcengineering/ui'
import tagsPlugin from '../plugin'
import TagReferencePresenter from './TagReferencePresenter.svelte'
import TagsEditorPopup from './TagsEditorPopup.svelte'
import TagIcon from './icons/TagIcon.svelte'
export let object: Doc
export let targetClass: Ref<Class<Doc>>
export let kind: ButtonKind = 'ghost'
let items: TagReference[] = []
const query = createQuery()
const client = getClient()
$: query.query(tags.class.TagReference, { attachedTo: object._id }, (result) => {
items = result
})
async function click (evt: MouseEvent): Promise<void> {
showPopup(TagsEditorPopup, { object, targetClass }, getEventPopupPositionElement(evt))
}
async function removeTag (tag: TagReference): Promise<void> {
if (tag !== undefined) await client.remove(tag)
}
</script>
<div>
<Button {kind} padding={'0rem;'} on:click={click}>
<div slot="content" class="flex-row-center flex-gap-1">
<Icon icon={TagIcon} size={'medium'} />
<span class="overflow-label label"><Label label={tagsPlugin.string.AddLabel} /></span>
</div>
</Button>
{#if items.length}
<div class="flex-row-center flex-wrap">
{#each items as value}
<div class="step-container clear-mins">
<TagReferencePresenter
attr={undefined}
isEditable
{value}
kind={'list'}
on:remove={(res) => removeTag(res.detail)}
/>
</div>
{/each}
</div>
{/if}
</div>
<style lang="scss">
.step-container {
margin: 0.375rem 0.375rem 0 0;
}
</style>

View File

@ -0,0 +1,72 @@
<!--
// Copyright © 2023 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 { Class, Doc, Ref } from '@hcengineering/core'
import { TagReference } from '@hcengineering/tags'
import { Button, ButtonKind, Icon, Label, getEventPopupPositionElement, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import tagsPlugin from '../plugin'
import DraftTagsPopup from './DraftTagsPopup.svelte'
import TagReferencePresenter from './TagReferencePresenter.svelte'
import TagIcon from './icons/TagIcon.svelte'
export let tags: TagReference[] = []
export let targetClass: Ref<Class<Doc>>
export let kind: ButtonKind = 'ghost'
const dispatch = createEventDispatcher()
function removeTag (tag: TagReference) {
tags = tags.filter((t) => t !== tag)
dispatch('change', tags)
}
function click (evt: MouseEvent) {
showPopup(DraftTagsPopup, { targetClass, tags }, getEventPopupPositionElement(evt), undefined, (res) => {
tags = res
dispatch('change', tags)
})
}
</script>
<div>
<Button {kind} padding={'0rem;'} on:click={click}>
<div slot="content" class="flex-row-center flex-gap-1">
<Icon icon={TagIcon} size={'medium'} />
<span class="overflow-label label"><Label label={tagsPlugin.string.AddLabel} /></span>
</div>
</Button>
{#if tags.length}
<div class="flex-row-center flex-wrap">
{#each tags as value}
<div class="step-container clear-mins">
<TagReferencePresenter
attr={undefined}
isEditable
{value}
kind={'list'}
on:remove={(res) => removeTag(res.detail)}
/>
</div>
{/each}
</div>
{/if}
</div>
<style lang="scss">
.step-container {
margin: 0.375rem 0.375rem 0 0;
}
</style>

View File

@ -0,0 +1,51 @@
<!--
// Copyright © 2023 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 { AttachedData, Class, Doc, Ref } from '@hcengineering/core'
import { TagElement, TagReference } from '@hcengineering/tags'
import { createEventDispatcher } from 'svelte'
import TagsPopup from './TagsPopup.svelte'
export let targetClass: Ref<Class<Doc>>
export let tags: AttachedData<TagReference>[] = []
$: selected = tags.map((p) => p.tag)
const dispatch = createEventDispatcher()
async function addRef ({ title, color, _id: tag }: TagElement): Promise<void> {
tags.push({
tag,
title,
color
})
tags = tags
dispatch('update', tags)
}
async function removeTag (tag: TagElement): Promise<void> {
tags = tags.filter((t) => t.tag !== tag._id)
tags = tags
dispatch('update', tags)
}
async function onUpdate (event: CustomEvent<{ action: string; tag: TagElement }>) {
const result = event.detail
if (result === undefined) return
if (result.action === 'add') addRef(result.tag)
else if (result.action === 'remove') removeTag(result.tag)
}
</script>
<TagsPopup {targetClass} {selected} on:update={onUpdate} />

View File

@ -0,0 +1,36 @@
<!--
// Copyright © 2023 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 { TagElement } from '@hcengineering/tags'
import { getPlatformColorDef, themeStore } from '@hcengineering/ui'
export let element: TagElement
$: color = getPlatformColorDef(element.color ?? 0, $themeStore.dark)
</script>
<div class="color" style:background-color={color.color} />
<span class="label overflow-label ml-1-5 max-w-40">
{element.title}
</span>
<style lang="scss">
.color {
flex-shrink: 0;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
}
</style>

View File

@ -1,17 +1,19 @@
<script lang="ts">
import { AnyAttribute, Doc } from '@hcengineering/core'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { TagReference } from '@hcengineering/tags'
import tags from '@hcengineering/tags'
import { Icon, IconAdd, Label, getEventPopupPositionElement, showPopup } from '@hcengineering/ui'
import { Icon, Label, getEventPopupPositionElement, showPopup } from '@hcengineering/ui'
import TagReferencePresenter from './TagReferencePresenter.svelte'
import TagsEditorPopup from './TagsEditorPopup.svelte'
import TagIcon from './icons/TagIcon.svelte'
export let object: Doc
export let label: IntlString
export let label: IntlString = tags.string.AddLabel
export let readonly: boolean = false
export let attr: AnyAttribute | undefined = undefined
export let targetClass: Ref<Class<Doc>> = object._class
let items: TagReference[] = []
const query = createQuery()
@ -22,7 +24,7 @@
})
async function tagsHandler (evt: MouseEvent): Promise<void> {
if (readonly) return
showPopup(TagsEditorPopup, { object }, getEventPopupPositionElement(evt))
showPopup(TagsEditorPopup, { object, targetClass }, getEventPopupPositionElement(evt))
}
async function removeTag (tag: TagReference): Promise<void> {
if (tag !== undefined) await client.remove(tag)
@ -45,7 +47,7 @@
{#if !readonly}
<div class="step-container clear-mins">
<button class="tag-button" on:click|stopPropagation={tagsHandler}>
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
<div class="icon"><Icon icon={TagIcon} size={'full'} /></div>
<span class="overflow-label label"><Label {label} /></span>
</button>
</div>
@ -53,7 +55,7 @@
</div>
{:else if !readonly}
<button class="tag-button" style="width: min-content" on:click|stopPropagation={tagsHandler}>
<div class="icon"><Icon icon={IconAdd} size={'full'} /></div>
<div class="icon"><Icon icon={TagIcon} size={'full'} /></div>
<span class="overflow-label label"><Label {label} /></span>
</button>
{/if}

View File

@ -13,12 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { Doc, Ref } from '@hcengineering/core'
import { Class, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags, { TagElement } from '@hcengineering/tags'
import TagsPopup from './TagsPopup.svelte'
export let object: Doc
export let targetClass: Ref<Class<Doc>> = object._class
let selected: Ref<TagElement>[] = []
const query = createQuery()
@ -45,4 +46,4 @@
}
</script>
<TagsPopup targetClass={object._class} {selected} on:update={onUpdate} />
<TagsPopup {targetClass} {selected} on:update={onUpdate} />

View File

@ -0,0 +1,22 @@
<script lang="ts">
export let size: 'x-small' | 'small' | 'medium' | 'large'
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" fill="none" viewBox="0 0 16 16">
<path
opacity="0.01"
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 16L16 16L16 2.54292e-07L-6.99382e-07 9.53674e-07L0 16Z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.28273 14.1415L1.86168 9.72159C1.63012 9.49034 1.5 9.17654 1.5 8.84932C1.5 8.5221 1.63012 8.20831 1.86168 7.97706L8.04102 1.79315C8.22858 1.60546 8.48305 1.5 8.74839 1.5L13.5 1.5C14.0523 1.5 14.5 1.94772 14.5 2.5L14.5 7.25015C14.5 7.5154 14.3946 7.76979 14.207 7.95733L8.02156 14.1415C7.54056 14.6195 6.76373 14.6195 6.28273 14.1415Z"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="11.4996" cy="4.49961" r="0.9" />
</svg>

View File

@ -12,7 +12,7 @@
// limitations under the License.
import { Resources } from '@hcengineering/platform'
import { TagElement } from '@hcengineering/tags'
import { TagElement as TagElementType } from '@hcengineering/tags'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import TagsCategoryBar from './components/CategoryBar.svelte'
import CategoryPresenter from './components/CategoryPresenter.svelte'
@ -32,13 +32,16 @@ import TagsEditorPopup from './components/TagsEditorPopup.svelte'
import LabelsPresenter from './components/LabelsPresenter.svelte'
import CreateTagElement from './components/CreateTagElement.svelte'
import ObjectsTagsEditorPopup from './components/ObjectsTagsEditorPopup.svelte'
import TagElement from './components/TagElement.svelte'
import { ObjQueryType } from '@hcengineering/core'
import { getRefs } from './utils'
import { Filter } from '@hcengineering/view'
import WeightPopup from './components/WeightPopup.svelte'
import DraftTagsEditor from './components/DraftTagsEditor.svelte'
import TagsFilterPresenter from './components/TagsFilterPresenter.svelte'
import DocTagsEditor from './components/DocTagsEditor.svelte'
export { WeightPopup }
export { WeightPopup, TagElement }
export async function tagsInResult (filter: Filter, onUpdate: () => void): Promise<ObjQueryType<any>> {
const result = await getRefs(filter, onUpdate)
return { $in: result }
@ -71,10 +74,12 @@ export default async (): Promise<Resources> => ({
TagsEditorPopup,
LabelsPresenter,
ObjectsTagsEditorPopup,
TagsFilterPresenter
TagsFilterPresenter,
DraftTagsEditor,
DocTagsEditor
},
actionImpl: {
Open: (value: TagElement, evt: MouseEvent) => {
Open: (value: TagElementType, evt: MouseEvent) => {
showPopup(EditTagElement, { value, keyTitle: '' }, eventToHTMLElement(evt))
}
},

View File

@ -35,7 +35,6 @@ export default mergeIds(tagsId, tags, {
WeightPlaceholder: '' as IntlString,
CategoryPlaceholder: '' as IntlString,
TagTooltip: '' as IntlString,
Tags: '' as IntlString,
Tag: '' as IntlString,
TagCreateLabel: '' as IntlString,
TagName: '' as IntlString,

View File

@ -14,7 +14,7 @@
//
import type { AttachedDoc, Class, Doc, Ref, Space } from '@hcengineering/core'
import type { Asset, Plugin } from '@hcengineering/platform'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import { writable } from 'svelte/store'
@ -100,6 +100,8 @@ const tagsPlugin = plugin(tagsId, {
Level3: '' as Asset
},
component: {
DraftTagsEditor: '' as AnyComponent,
DocTagsEditor: '' as AnyComponent,
TagsView: '' as AnyComponent,
TagsEditor: '' as AnyComponent,
TagsDropdownEditor: '' as AnyComponent,
@ -111,6 +113,11 @@ const tagsPlugin = plugin(tagsId, {
TagsEditorPopup: '' as AnyComponent,
ObjectsTagsEditorPopup: '' as AnyComponent
},
string: {
Tags: '' as IntlString,
AddLabel: '' as IntlString,
TagLabel: '' as IntlString
},
category: {
NoCategory: '' as Ref<TagCategory>
},