diff --git a/models/card/src/actions.ts b/models/card/src/actions.ts new file mode 100644 index 0000000000..f66c26fbc1 --- /dev/null +++ b/models/card/src/actions.ts @@ -0,0 +1,218 @@ +// Copyright © 2025 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 Builder } from '@hcengineering/model' +import presentation from '@hcengineering/model-presentation' +import setting from '@hcengineering/model-setting' +import view, { createAction } from '@hcengineering/model-view' +import workbench from '@hcengineering/model-workbench' +import card from './plugin' + +export function createActions (builder: Builder): void { + createAction(builder, { + action: card.actionImpl.EditSpace, + label: presentation.string.Edit, + icon: view.icon.Edit, + input: 'focus', + category: view.category.General, + target: card.class.CardSpace, + visibilityTester: view.function.CanEditSpace, + query: {}, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }) + + createAction(builder, { + action: view.actionImpl.CopyTextToClipboard, + actionProps: { + textProvider: card.function.GetSpaceAccessPublicLink + }, + label: card.string.GetIndividualPublicLink, + icon: view.icon.CopyLink, + input: 'any', + category: view.category.General, + target: card.class.CardSpace, + query: {}, + visibilityTester: card.function.CanGetSpaceAccessPublicLink, + context: { + mode: ['context', 'browser'] + } + }) + + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: card.component.SetParentActionPopup, + element: 'top', + fillProps: { + _objects: 'value' + } + }, + label: card.string.SetParent, + icon: card.icon.MasterTag, + input: 'none', + category: card.category.Card, + target: card.class.Card, + context: { + mode: ['context'], + application: card.app.Card, + group: 'associate' + } + }, + card.action.SetParent + ) + + createAction( + builder, + { + action: view.actionImpl.UpdateDocument, + actionProps: { + key: 'parent', + value: null + }, + query: { + parent: { $ne: null, $exists: true } + }, + label: card.string.UnsetParent, + icon: card.icon.MasterTag, + input: 'none', + category: card.category.Card, + target: card.class.Card, + context: { + mode: ['context'], + application: card.app.Card, + group: 'associate' + } + }, + card.action.UnsetParent + ) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: card.component.CreateTag, + props: { + _class: card.class.Tag + }, + fillProps: { + _object: 'parent' + } + }, + label: card.string.CreateTag, + input: 'focus', + icon: view.icon.Add, + category: setting.category.Settings, + target: card.class.MasterTag, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: card.component.CreateTag, + props: { + _class: card.class.Tag + }, + fillProps: { + _object: 'parent' + } + }, + label: card.string.CreateTag, + input: 'focus', + icon: view.icon.Add, + category: setting.category.Settings, + target: card.class.Tag, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }) + + createAction( + builder, + { + action: card.actionImpl.DeleteMasterTag, + label: workbench.string.Delete, + icon: view.icon.Delete, + input: 'any', + category: view.category.General, + target: card.class.MasterTag, + context: { + mode: ['context', 'browser'], + group: 'remove' + } + }, + card.action.DeleteMasterTag + ) + + createAction(builder, { + action: card.actionImpl.DeleteMasterTag, + label: workbench.string.Delete, + icon: view.icon.Delete, + input: 'any', + category: view.category.General, + target: card.class.Tag, + context: { + mode: ['context', 'browser'], + group: 'remove' + } + }) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: card.component.ChangeType, + fillProps: { + _object: 'value' + } + }, + label: card.string.ChangeType, + input: 'focus', + icon: card.icon.MasterTag, + category: setting.category.Settings, + target: card.class.Card, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }) + + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: card.component.CreateTag, + props: { + _class: card.class.MasterTag + }, + fillProps: { + _object: 'parent' + } + }, + label: card.string.CreateMasterTag, + input: 'focus', + icon: card.icon.MasterTag, + category: setting.category.Settings, + target: card.class.MasterTag, + context: { + mode: ['context', 'browser'], + group: 'edit' + } + }) +} diff --git a/models/card/src/index.ts b/models/card/src/index.ts index f0934f817e..d46e1fbd96 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -13,6 +13,7 @@ import activity from '@hcengineering/activity' import { + type CanCreateCardResource, type Card, cardId, type CardNavigation, @@ -20,7 +21,6 @@ import { type CardSpace, type CardViewDefaults, type CreateCardExtension, - type CanCreateCardResource, DOMAIN_CARD, type FavoriteCard, type MasterTag, @@ -60,17 +60,18 @@ import { import attachment from '@hcengineering/model-attachment' import { TAttachedDoc, TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core' import { createPublicLinkAction } from '@hcengineering/model-guest' +import preference, { TPreference } from '@hcengineering/model-preference' import presentation from '@hcengineering/model-presentation' import setting from '@hcengineering/model-setting' -import view, { createAction, type Viewlet } from '@hcengineering/model-view' +import view, { type Viewlet } from '@hcengineering/model-view' import workbench, { WidgetType } from '@hcengineering/model-workbench' import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform' import time, { type ToDo } from '@hcengineering/time' +import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' import { type AnyComponent } from '@hcengineering/ui/src/types' import { type BuildModelKey } from '@hcengineering/view' -import preference, { TPreference } from '@hcengineering/model-preference' import card from './plugin' -import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' +import { createActions } from './actions' export { cardId } from '@hcengineering/card' @@ -399,37 +400,7 @@ export function createModel (builder: Builder): void { } }) - createAction(builder, { - action: card.actionImpl.EditSpace, - label: presentation.string.Edit, - icon: view.icon.Edit, - input: 'focus', - category: view.category.General, - target: card.class.CardSpace, - visibilityTester: view.function.CanEditSpace, - query: {}, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }) - - createAction(builder, { - action: view.actionImpl.CopyTextToClipboard, - actionProps: { - textProvider: card.function.GetSpaceAccessPublicLink - }, - label: card.string.GetIndividualPublicLink, - icon: view.icon.CopyLink, - input: 'any', - category: view.category.General, - target: card.class.CardSpace, - query: {}, - visibilityTester: card.function.CanGetSpaceAccessPublicLink, - context: { - mode: ['context', 'browser'] - } - }) + createActions(builder) builder.mixin(card.class.CardSpace, core.class.Class, workbench.mixin.SpaceView, { view: { @@ -497,31 +468,6 @@ export function createModel (builder: Builder): void { presenter: card.component.SpacePresenter }) - createAction( - builder, - { - action: view.actionImpl.ShowPopup, - actionProps: { - component: card.component.SetParentActionPopup, - element: 'top', - fillProps: { - _objects: 'value' - } - }, - label: card.string.SetParent, - icon: card.icon.MasterTag, - input: 'none', - category: card.category.Card, - target: card.class.Card, - context: { - mode: ['context'], - application: card.app.Card, - group: 'associate' - } - }, - card.action.SetParent - ) - builder.mixin(card.class.Card, core.class.Class, view.mixin.AttributeEditor, { inlineEditor: card.component.CardEditor }) @@ -530,31 +476,6 @@ export function createModel (builder: Builder): void { inlineEditor: card.component.CardArrayEditor }) - createAction( - builder, - { - action: view.actionImpl.UpdateDocument, - actionProps: { - key: 'parent', - value: null - }, - query: { - parent: { $ne: null, $exists: true } - }, - label: card.string.UnsetParent, - icon: card.icon.MasterTag, - input: 'none', - category: card.category.Card, - target: card.class.Card, - context: { - mode: ['context'], - application: card.app.Card, - group: 'associate' - } - }, - card.action.UnsetParent - ) - builder.createDoc( view.class.Viewlet, core.space.Model, @@ -693,50 +614,6 @@ export function createModel (builder: Builder): void { encode: card.function.GetCardLink }) - createAction(builder, { - action: view.actionImpl.ShowPopup, - actionProps: { - component: card.component.CreateTag, - props: { - _class: card.class.Tag - }, - fillProps: { - _object: 'parent' - } - }, - label: card.string.CreateTag, - input: 'focus', - icon: view.icon.Add, - category: setting.category.Settings, - target: card.class.MasterTag, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }) - - createAction(builder, { - action: view.actionImpl.ShowPopup, - actionProps: { - component: card.component.CreateTag, - props: { - _class: card.class.Tag - }, - fillProps: { - _object: 'parent' - } - }, - label: card.string.CreateTag, - input: 'focus', - icon: view.icon.Add, - category: setting.category.Settings, - target: card.class.Tag, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }) - builder.mixin(card.class.MasterTag, core.class.Class, view.mixin.IgnoreActions, { actions: [setting.action.CreateMixin, view.action.OpenInNewTab, view.action.Delete, setting.action.DeleteMixin] }) @@ -745,77 +622,6 @@ export function createModel (builder: Builder): void { actions: [setting.action.CreateMixin, view.action.OpenInNewTab, view.action.Delete, setting.action.DeleteMixin] }) - createAction( - builder, - { - action: card.actionImpl.DeleteMasterTag, - label: workbench.string.Delete, - icon: view.icon.Delete, - input: 'any', - category: view.category.General, - target: card.class.MasterTag, - context: { - mode: ['context', 'browser'], - group: 'remove' - } - }, - card.action.DeleteMasterTag - ) - - createAction(builder, { - action: card.actionImpl.DeleteMasterTag, - label: workbench.string.Delete, - icon: view.icon.Delete, - input: 'any', - category: view.category.General, - target: card.class.Tag, - context: { - mode: ['context', 'browser'], - group: 'remove' - } - }) - - createAction(builder, { - action: view.actionImpl.ShowPopup, - actionProps: { - component: card.component.ChangeType, - fillProps: { - _object: 'value' - } - }, - label: card.string.ChangeType, - input: 'focus', - icon: card.icon.MasterTag, - category: setting.category.Settings, - target: card.class.Card, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }) - - createAction(builder, { - action: view.actionImpl.ShowPopup, - actionProps: { - component: card.component.CreateTag, - props: { - _class: card.class.MasterTag - }, - fillProps: { - _object: 'parent' - } - }, - label: card.string.CreateMasterTag, - input: 'focus', - icon: card.icon.MasterTag, - category: setting.category.Settings, - target: card.class.MasterTag, - context: { - mode: ['context', 'browser'], - group: 'edit' - } - }) - builder.createDoc( setting.class.WorkspaceSettingCategory, core.space.Model, diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 7e90b48057..5ca2d0d41f 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -835,6 +835,25 @@ export function createModel (builder: Builder): void { view.action.Join ) + createAction(builder, { + action: view.actionImpl.ShowPopup, + actionProps: { + component: view.component.AddRelationPopup, + fillProps: { + _objects: 'value' + } + }, + label: core.string.AddRelation, + input: 'any', + icon: view.icon.CopyLink, + category: view.category.Editor, + target: core.class.Doc, + context: { + mode: ['context', 'browser'], + group: 'associate' + } + }) + createAction( builder, { diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index d756b561fa..448c0c4bbe 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -96,7 +96,8 @@ export default mergeIds(viewId, view, { BaseDocPresenter: '' as AnyComponent, MasterDetailView: '' as AnyComponent, AssociationPresenter: '' as AnyComponent, - TreeView: '' as AnyComponent + TreeView: '' as AnyComponent, + AddRelationPopup: '' as AnyComponent }, string: { Table: '' as IntlString, diff --git a/plugins/view-resources/src/components/relation/AddRelationPopup.svelte b/plugins/view-resources/src/components/relation/AddRelationPopup.svelte new file mode 100644 index 0000000000..6bbf559b50 --- /dev/null +++ b/plugins/view-resources/src/components/relation/AddRelationPopup.svelte @@ -0,0 +1,127 @@ + + + + { + dispatch('close') + }} + on:changeContent +> +
+ (target = undefined)} /> +
+ {#if selected !== undefined} + + {/if} +
diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index b38331d57e..fa37305b87 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -20,7 +20,9 @@ import { canCopyLink } from '@hcengineering/view' import { actionImpl } from './actionImpl' import ActionsPopup from './components/ActionsPopup.svelte' import ArrayEditor from './components/ArrayEditor.svelte' +import AssociationPresenter from './components/AssociationPresenter.svelte' import AttachedDocPanel from './components/AttachedDocPanel.svelte' +import BaseDocPresenter from './components/BaseDocPresenter.svelte' import BooleanEditor from './components/BooleanEditor.svelte' import BooleanPresenter from './components/BooleanPresenter.svelte' import BooleanTruePresenter from './components/BooleanTruePresenter.svelte' @@ -59,6 +61,9 @@ import NumberPresenter from './components/NumberPresenter.svelte' import ObjectIcon from './components/ObjectIcon.svelte' import ObjectMention from './components/ObjectMention.svelte' import ObjectPresenter from './components/ObjectPresenter.svelte' +import PersonArrayEditor from './components/PersonArrayEditor.svelte' +import PersonIdPresenter from './components/PersonIdPresenter.svelte' +import ReadOnlyNotification from './components/ReadOnlyNotification.svelte' import RolePresenter from './components/RolePresenter.svelte' import SearchSelector from './components/SearchSelector.svelte' import SpaceHeader from './components/SpaceHeader.svelte' @@ -70,21 +75,25 @@ import StringPresenter from './components/StringPresenter.svelte' import Table from './components/Table.svelte' import TableBrowser from './components/TableBrowser.svelte' import TimestampPresenter from './components/TimestampPresenter.svelte' +import TreeView from './components/TreeView.svelte' import UpDownNavigator from './components/UpDownNavigator.svelte' import ValueSelector from './components/ValueSelector.svelte' import ViewletContentView from './components/ViewletContentView.svelte' -import ViewletSettingButton from './components/ViewletSettingButton.svelte' import ViewletPanelHeader from './components/ViewletPanelHeader.svelte' +import ViewletSettingButton from './components/ViewletSettingButton.svelte' import ArrayFilter from './components/filter/ArrayFilter.svelte' import DateFilter from './components/filter/DateFilter.svelte' import DateFilterPresenter from './components/filter/DateFilterPresenter.svelte' import FilterBar from './components/filter/FilterBar.svelte' import FilterTypePopup from './components/filter/FilterTypePopup.svelte' import ObjectFilter from './components/filter/ObjectFilter.svelte' +import PersonIdFilter from './components/filter/PersonIdFilter.svelte' +import PersonIdFilterValuePresenter from './components/filter/PersonIdFilterValuePresenter.svelte' import StringFilter from './components/filter/StringFilter.svelte' import StringFilterPresenter from './components/filter/StringFilterPresenter.svelte' import TimestampFilter from './components/filter/TimestampFilter.svelte' import ValueFilter from './components/filter/ValueFilter.svelte' +import FoldersBrowser from './components/folders/FoldersBrowser.svelte' import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte' import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte' import DividerPresenter from './components/list/DividerPresenter.svelte' @@ -93,27 +102,19 @@ import ListView from './components/list/ListView.svelte' import SortableDocList from './components/list/SortableDocList.svelte' import SortableList from './components/list/SortableList.svelte' import SortableListItem from './components/list/SortableListItem.svelte' +import MasterDetailBrowser from './components/masterDetail/MasterDetailBrowser.svelte' +import MasterDetailView from './components/masterDetail/MasterDetailView.svelte' import TreeElement from './components/navigator/TreeElement.svelte' import TreeItem from './components/navigator/TreeItem.svelte' import TreeNode from './components/navigator/TreeNode.svelte' +import AddRelationPopup from './components/relation/AddRelationPopup.svelte' import StatusPresenter from './components/status/StatusPresenter.svelte' import StatusRefPresenter from './components/status/StatusRefPresenter.svelte' -import PersonArrayEditor from './components/PersonArrayEditor.svelte' -import PersonIdPresenter from './components/PersonIdPresenter.svelte' -import PersonIdFilter from './components/filter/PersonIdFilter.svelte' -import PersonIdFilterValuePresenter from './components/filter/PersonIdFilterValuePresenter.svelte' import AudioViewer from './components/viewer/AudioViewer.svelte' import ImageViewer from './components/viewer/ImageViewer.svelte' -import VideoViewer from './components/viewer/VideoViewer.svelte' import PDFViewer from './components/viewer/PDFViewer.svelte' import TextViewer from './components/viewer/TextViewer.svelte' -import FoldersBrowser from './components/folders/FoldersBrowser.svelte' -import BaseDocPresenter from './components/BaseDocPresenter.svelte' -import MasterDetailView from './components/masterDetail/MasterDetailView.svelte' -import AssociationPresenter from './components/AssociationPresenter.svelte' -import TreeView from './components/TreeView.svelte' -import MasterDetailBrowser from './components/masterDetail/MasterDetailBrowser.svelte' -import ReadOnlyNotification from './components/ReadOnlyNotification.svelte' +import VideoViewer from './components/viewer/VideoViewer.svelte' import { blobImageMetadata, blobVideoMetadata } from './blob' @@ -140,7 +141,9 @@ import { valueNinResult } from './filter' +import ForbiddenNotification from './components/ForbiddenNotification.svelte' import { AggregationMiddleware, AnalyticsMiddleware, ReadOnlyAccessMiddleware } from './middleware' +import { getLink, openDocFromRef } from './utils' import { hideArchived, showEmptyGroups } from './viewOptions' import { canArchiveSpace, @@ -151,9 +154,6 @@ import { canLeaveSpace, isClipboardAvailable } from './visibilityTester' -import { getLink, openDocFromRef } from './utils' -import ForbiddenNotification from './components/ForbiddenNotification.svelte' -export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester' export { getActions, getContextActions, invokeAction, showMenu } from './actions' export { default as ActionButton } from './components/ActionButton.svelte' export { default as ActionHandler } from './components/ActionHandler.svelte' @@ -170,33 +170,34 @@ export { default as ObjectBoxPopup } from './components/ObjectBoxPopup.svelte' export { default as ObjectPresenter } from './components/ObjectPresenter.svelte' export { default as ObjectSearchBox } from './components/ObjectSearchBox.svelte' export { default as ParentsNavigator } from './components/ParentsNavigator.svelte' +export { default as PersonIdPresenter } from './components/PersonIdPresenter.svelte' +export { default as RelationsEditor } from './components/RelationsEditor.svelte' export { default as SpaceTypeSelector } from './components/SpaceTypeSelector.svelte' export { default as TableBrowser } from './components/TableBrowser.svelte' export { default as ValueSelector } from './components/ValueSelector.svelte' +export { default as ViewOptions } from './components/ViewOptions.svelte' +export { default as ViewletClassSettings } from './components/ViewletClassSettings.svelte' export { default as ViewletSelector } from './components/ViewletSelector.svelte' export { default as ViewletsSettingButton } from './components/ViewletsSettingButton.svelte' export { default as FilterButton } from './components/filter/FilterButton.svelte' export { default as FilterRemovedNotification } from './components/filter/FilterRemovedNotification.svelte' -export { default as List } from './components/list/List.svelte' -export { default as NavLink } from './components/navigator/NavLink.svelte' -export { default as StatusPresenter } from './components/status/StatusPresenter.svelte' -export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte' -export { default as PersonIdPresenter } from './components/PersonIdPresenter.svelte' export { default as PersonIdFilter } from './components/filter/PersonIdFilter.svelte' export { default as PersonIdFilterValuePresenter } from './components/filter/PersonIdFilterValuePresenter.svelte' export { default as FoldersBrowser } from './components/folders/FoldersBrowser.svelte' -export { default as RelationsEditor } from './components/RelationsEditor.svelte' +export { default as List } from './components/list/List.svelte' export { default as ListView } from './components/list/ListView.svelte' -export { default as ViewOptions } from './components/ViewOptions.svelte' -export { default as ViewletClassSettings } from './components/ViewletClassSettings.svelte' +export { default as NavLink } from './components/navigator/NavLink.svelte' +export { default as StatusPresenter } from './components/status/StatusPresenter.svelte' +export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte' +export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester' export * from './filter' +export * from './icons' export * from './middleware' +export * from './objectIterator' export * from './selection' export * from './status' export * from './utils' -export * from './icons' -export * from './objectIterator' export { buildModel, getActiveViewletId, @@ -236,8 +237,8 @@ export { NumberPresenter, ObjectIcon, ObjectMention, - SortableList, SortableDocList, + SortableList, SortableListItem, SpaceHeader, SpacePresenter, @@ -250,8 +251,8 @@ export { TreeNode, UpDownNavigator, ViewletContentView, - ViewletSettingButton, - ViewletPanelHeader + ViewletPanelHeader, + ViewletSettingButton } function PositionElementAlignment (e?: Event): PopupAlignment | undefined { @@ -261,6 +262,7 @@ function PositionElementAlignment (e?: Event): PopupAlignment | undefined { export default async (): Promise => ({ actionImpl, component: { + AddRelationPopup, ArrayFilter, ArrayEditor, ClassPresenter, diff --git a/server-plugins/process-resources/src/functions.ts b/server-plugins/process-resources/src/functions.ts index a6657bfbcf..32fb0da34d 100644 --- a/server-plugins/process-resources/src/functions.ts +++ b/server-plugins/process-resources/src/functions.ts @@ -120,7 +120,7 @@ export async function AddRelation ( params: MethodParams, execution: Execution, control: ProcessControl -): Promise { +): Promise { const _id = generateId() const association = params.association as Ref if (isEmpty(association)) { @@ -141,6 +141,14 @@ export async function AddRelation ( docA, docB } + const exists = await control.client.findOne(core.class.Relation, { docA, docB, association }) + if (exists !== undefined) { + return { + txes: [], + rollback: [], + context: [] + } + } const resTx = control.client.txFactory.createTxCreateDoc(core.class.Relation, core.space.Workspace, data, _id) const res: Tx[] = [resTx] const rollback: Tx[] = [control.client.txFactory.createTxRemoveDoc(core.class.Relation, core.space.Workspace, _id)]