EQMS-1451/1453/1455: reference tweaks for QMS documents (#8083)

* EQMS-1451/1453/1455: reference tweaks for QMS documents

Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>

* fmt

Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>

---------

Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
Victor Ilyushchenko
2025-02-24 17:13:35 +03:00
committed by GitHub
parent f98152986c
commit c6e752c7aa
22 changed files with 195 additions and 40 deletions
+13 -1
View File
@@ -106,7 +106,7 @@ export function createModel (builder: Builder): void {
})
builder.mixin(documents.class.DocumentMeta, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: documents.function.ControlledDocumentTitleProvider
titleProvider: documents.function.DocumentMetaTitleProvider
})
builder.mixin(documents.class.DocumentApprovalRequest, core.class.Class, view.mixin.ObjectPresenter, {
@@ -406,6 +406,10 @@ export function createModel (builder: Builder): void {
presenter: documents.component.DocumentPresenter
})
builder.mixin(documents.class.ControlledDocument, core.class.Class, view.mixin.LinkProvider, {
encode: documents.function.GetControlledDocumentLinkFragment
})
builder.mixin(documents.class.Document, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Delete]
})
@@ -741,6 +745,14 @@ export function createModel (builder: Builder): void {
provider: documents.function.DocumentIdentifierProvider
})
builder.mixin(documents.class.ControlledDocument, core.class.Class, view.mixin.ReferenceObjectProvider, {
provider: documents.function.ControlledDocumentReferenceObjectProvider
})
builder.mixin(documents.class.ProjectDocument, core.class.Class, view.mixin.ReferenceObjectProvider, {
provider: documents.function.ProjectDocumentReferenceObjectProvider
})
createAction(
builder,
{
@@ -53,6 +53,12 @@ export default mergeIds(documentsId, documents, {
DocumentIdentifierProvider: '' as Resource<
<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>
>,
ControlledDocumentReferenceObjectProvider: '' as Resource<
<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<Doc>
>,
ProjectDocumentReferenceObjectProvider: '' as Resource<
<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<Doc>
>,
Comment: '' as Resource<TextActionFunction>,
IsCommentVisible: '' as Resource<TextActionVisibleFunction>
},
+7
View File
@@ -89,6 +89,7 @@ import {
type ViewletDescriptor,
type ViewletPreference,
type ObjectIdentifier,
type ReferenceObjectProvider,
type ObjectIcon,
type ObjectTooltip,
type AttrPresenter,
@@ -253,6 +254,11 @@ export class TObjectIdentifier extends TClass implements ObjectIdentifier {
provider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>
}
@Mixin(view.mixin.ReferenceObjectProvider, core.class.Class)
export class TReferenceObjectProvider extends TClass implements ReferenceObjectProvider {
provider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<Doc | undefined>>
}
@Mixin(view.mixin.ObjectTooltip, core.class.Class)
export class TObjectTooltip extends TClass implements ObjectTooltip {
provider!: Resource<(client: Client, doc?: Doc | null) => Promise<LabelAndProps | undefined>>
@@ -465,6 +471,7 @@ export function createModel (builder: Builder): void {
TAggregation,
TGroupping,
TObjectIdentifier,
TReferenceObjectProvider,
TObjectTooltip,
TObjectIcon,
TAttrPresenter,
@@ -300,7 +300,9 @@
"Obsolete": "Zastaralé",
"MakeDocumentObsolete": "Označit jako zastaralé",
"MakeDocumentObsoleteDialog": "Označit {count, plural, one {dokument jako zastaralý} other {dokumenty jako zastaralé}}",
"MakeDocumentObsoleteConfirm": "Opravdu chcete označit následující dokumenty jako zastaralé: {titles}?"
"MakeDocumentObsoleteConfirm": "Opravdu chcete označit následující dokumenty jako zastaralé: {titles}?",
"LatestVersionHint": "nejnovější"
},
"controlledDocStates": {
"Empty": "",
@@ -307,7 +307,9 @@
"Obsolete": "Veraltet",
"MakeDocumentObsolete": "Als veraltet markieren",
"MakeDocumentObsoleteDialog": "{count, plural, one {Dokument als veraltet markieren} other {Dokumente als veraltet markieren}}",
"MakeDocumentObsoleteConfirm": "Möchten Sie die folgenden Dokumente wirklich als veraltet markieren: {titles}?"
"MakeDocumentObsoleteConfirm": "Möchten Sie die folgenden Dokumente wirklich als veraltet markieren: {titles}?",
"LatestVersionHint": "neueste"
},
"controlledDocStates": {
"Empty": "",
@@ -309,7 +309,9 @@
"CreateFolder": "Create new folder",
"RenameFolder": "Rename folder",
"CreateChildFolder": "Create child folder"
"CreateChildFolder": "Create child folder",
"LatestVersionHint": "latest"
},
"controlledDocStates": {
"Empty": "",
@@ -267,7 +267,9 @@
"Obsolete": "Obsolète",
"MakeDocumentObsolete": "Marquer comme obsolète",
"MakeDocumentObsoleteDialog": "Marquer {count, plural, one {le document comme obsolète} other {les documents comme obsolètes}}",
"MakeDocumentObsoleteConfirm": "Voulez-vous vraiment marquer les documents suivants comme obsolètes : {titles} ?"
"MakeDocumentObsoleteConfirm": "Voulez-vous vraiment marquer les documents suivants comme obsolètes : {titles} ?",
"LatestVersionHint": "dernier"
},
"controlledDocStates": {
"Empty": "",
@@ -265,7 +265,9 @@
"Obsolete": "Obsoleto",
"MakeDocumentObsolete": "Segna come obsoleto",
"MakeDocumentObsoleteDialog": "Segna {count, plural, one {il documento come obsoleto} other {i documenti come obsoleti}}",
"MakeDocumentObsoleteConfirm": "Vuoi davvero segnare i seguenti documenti come obsoleti: {titles}?"
"MakeDocumentObsoleteConfirm": "Vuoi davvero segnare i seguenti documenti come obsoleti: {titles}?",
"LatestVersionHint": "ultimo"
},
"controlledDocStates": {
"Empty": "",
@@ -309,7 +309,9 @@
"Obsolete": "Устаревший",
"MakeDocumentObsolete": "Пометить как устаревшее",
"MakeDocumentObsoleteDialog": "Пометить {count, plural, one {документ как устаревший} other {документы как устаревшие}}",
"MakeDocumentObsoleteConfirm": "Вы действительно хотите пометить следующие документы как устаревшие: {titles}?"
"MakeDocumentObsoleteConfirm": "Вы действительно хотите пометить следующие документы как устаревшие: {titles}?",
"LatestVersionHint": "последняя"
},
"controlledDocStates": {
"Empty": "",
@@ -306,7 +306,9 @@
"Obsolete": "已过时",
"MakeDocumentObsolete": "标记为过时",
"MakeDocumentObsoleteDialog": "标记 {count, plural, one {文档为过时} other {文档为过时}}",
"MakeDocumentObsoleteConfirm": "您确定要将以下文档标记为过时吗:{titles}?"
"MakeDocumentObsoleteConfirm": "您确定要将以下文档标记为过时吗:{titles}?",
"LatestVersionHint": "最新"
},
"controlledDocStates": {
"Empty": "",
@@ -49,10 +49,12 @@
$documentComments as documentComments,
documentCommentsDisplayRequested,
documentCommentsHighlightUpdated,
documentCommentsLocationNavigateRequested
documentCommentsLocationNavigateRequested,
$documentReleasedVersions as documentReleasedVersions
} from '../../stores/editors/document'
import DocumentTitle from './DocumentTitle.svelte'
import DocumentPrintTitlePage from '../print/DocumentPrintTitlePage.svelte'
import { syncDocumentMetaTitle } from '../../utils'
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -111,14 +113,17 @@
unsubscribeNavigateToLocation()
})
const handleUpdateTitle = () => {
const handleUpdateTitle = async () => {
if (!$controlledDocument || !title) {
return
}
const titleTrimmed = title.trim()
if (titleTrimmed.length > 0 && titleTrimmed !== $controlledDocument.title) {
client.update($controlledDocument, { title: titleTrimmed })
await client.update($controlledDocument, { title: titleTrimmed })
if ($documentReleasedVersions.length === 0 && $controlledDocument.state === DocumentState.Draft) {
await syncDocumentMetaTitle(client, $controlledDocument.attachedTo, $controlledDocument.code, titleTrimmed)
}
}
}
@@ -15,14 +15,15 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import documents, { Document } from '@hcengineering/controlled-documents'
import documents, { ControlledDocument } from '@hcengineering/controlled-documents'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { Button, EditBox, Label } from '@hcengineering/ui'
import IconWarning from '../../icons/IconWarning.svelte'
import documentsRes from '../../../plugin'
import { syncDocumentMetaTitle } from '../../../utils'
export let object: Document
export let object: ControlledDocument
const client = getClient()
const dispatch = createEventDispatcher()
@@ -59,6 +60,7 @@
}
await client.update(object, { code })
await syncDocumentMetaTitle(client, object.attachedTo, code, object.title)
dispatch('close')
}
</script>
@@ -106,7 +106,10 @@ import {
createTemplate,
deleteFolder,
documentIdentifierProvider,
controlledDocumentReferenceObjectProvider,
projectDocumentReferenceObjectProvider,
getAllDocumentStates,
getControlledDocumentLinkFragment,
getControlledDocumentTitle,
getDocumentMetaLinkFragment,
getDocumentMetaTitle,
@@ -450,6 +453,7 @@ export default async (): Promise<Resources> => ({
DocumentStateSort: sortDocumentStates,
GetAllDocumentStates: getAllDocumentStates,
GetDocumentMetaLinkFragment: getDocumentMetaLinkFragment,
GetControlledDocumentLinkFragment: getControlledDocumentLinkFragment,
CanDeleteDocument: canDeleteDocument,
CanArchiveDocument: canArchiveDocument,
CanMakeDocumentObsolete: canMakeDocumentObsolete,
@@ -457,6 +461,8 @@ export default async (): Promise<Resources> => ({
CanOpenDocument: canOpenDocument,
CanPrintDocument: canPrintDocument,
DocumentIdentifierProvider: documentIdentifierProvider,
ControlledDocumentReferenceObjectProvider: controlledDocumentReferenceObjectProvider,
ProjectDocumentReferenceObjectProvider: projectDocumentReferenceObjectProvider,
ControlledDocumentTitleProvider: getControlledDocumentTitle,
DocumentMetaTitleProvider: getDocumentMetaTitle,
Comment: comment,
@@ -212,7 +212,9 @@ export default mergeIds(documentsId, documents, {
CreateDocumentFailed: '' as IntlString,
CreateDocumentTemplateFailed: '' as IntlString,
TryAgain: '' as IntlString
TryAgain: '' as IntlString,
LatestVersionHint: '' as IntlString
},
controlledDocStates: {
Empty: '' as IntlString,
@@ -240,6 +242,7 @@ export default mergeIds(documentsId, documents, {
GetAllDocumentStates: '' as Resource<() => Promise<DocumentState[]>>,
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>,
GetDocumentMetaLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetControlledDocumentLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
CanDeleteDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanArchiveDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanMakeDocumentObsolete: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
@@ -16,6 +16,7 @@ import { type Employee, type Person, type PersonAccount } from '@hcengineering/c
import documents, {
type ControlledDocument,
type Document,
type DocumentBundle,
type DocumentCategory,
type DocumentComment,
type DocumentMeta,
@@ -27,12 +28,12 @@ import documents, {
type ProjectDocument,
type ProjectMeta,
ControlledDocumentState,
type DocumentBundle,
DocumentState,
ProjectDocumentTree,
compareDocumentVersions,
emptyBundle,
getDocumentName,
getFirstRank,
ProjectDocumentTree
getFirstRank
} from '@hcengineering/controlled-documents'
import core, {
type Class,
@@ -195,6 +196,18 @@ export async function getDocumentMetaLinkFragment (document: Doc): Promise<Locat
return getProjectDocumentLink(targetDocument, project)
}
export async function getControlledDocumentLinkFragment (document: ControlledDocument): Promise<Location> {
const client = getClient()
const targetDocument = await client.findOne(documents.class.ProjectDocument, { document: document._id })
if (targetDocument === undefined) {
throw new Error('Cannot resolve a ProjectDocument for document ' + document._id)
}
const project = targetDocument.project ?? documents.ids.NoProject
return getProjectDocumentLink(document, project)
}
export interface TeamPopupData {
controlledDoc: ControlledDocument
requestClass: Ref<Class<DocumentRequest>>
@@ -707,6 +720,41 @@ export async function documentIdentifierProvider (client: Client, ref: Ref<Docum
return document.code
}
export async function controlledDocumentReferenceObjectProvider (
client: Client,
ref: Ref<ControlledDocument>,
doc?: ControlledDocument
): Promise<Doc | undefined> {
const document = doc ?? (await client.findOne(documents.class.ControlledDocument, { _id: ref }))
if (document === undefined) return
const meta = await client.findOne(documents.class.DocumentMeta, { _id: document.attachedTo })
if (meta === undefined) return
let documentSeq: ControlledDocument[] = await client.findAll(documents.class.ControlledDocument, {
attachedTo: meta._id
})
const allowStates = [DocumentState.Draft, DocumentState.Effective]
documentSeq = documentSeq.filter((d) => allowStates.includes(d.state)).sort(compareDocumentVersions)
const effIndex = documentSeq.findIndex((d) => d.state === DocumentState.Effective)
const docIndex = documentSeq.findIndex((d) => d._id === document._id)
return docIndex >= 0 && (docIndex <= effIndex || effIndex < 0) ? meta : document
}
export async function projectDocumentReferenceObjectProvider (
client: Client,
ref: Ref<ProjectDocument>,
doc?: ProjectDocument
): Promise<Doc | undefined> {
const prjdoc = doc ?? (await client.findOne(documents.class.ProjectDocument, { _id: ref }))
if (prjdoc === undefined) return
return await controlledDocumentReferenceObjectProvider(client, prjdoc.document as Ref<ControlledDocument>)
}
export function documentCompareFn (doc1: Document, doc2: Document): number {
return doc1.major - doc2.major !== 0 ? doc1.major - doc2.major : doc1.minor - doc2.minor
}
@@ -724,7 +772,7 @@ export async function getControlledDocumentTitle (
if (object === undefined) return ''
return object.title
return object.title + ` (${getDocumentVersionString(object)})`
}
export async function getDocumentMetaTitle (
@@ -736,7 +784,9 @@ export async function getDocumentMetaTitle (
if (object === undefined) return ''
return object.title
const hint = await translate(documentsResources.string.LatestVersionHint, {})
return object.title + ` (${hint})`
}
export const getCurrentEmployee = (): Ref<Employee> | undefined => {
@@ -932,3 +982,15 @@ export class DocumentHiearchyQuery {
export function createDocumentHierarchyQuery (): DocumentHiearchyQuery {
return new DocumentHiearchyQuery()
}
export async function syncDocumentMetaTitle (
client: Client & TxOperations,
_id: Ref<DocumentMeta>,
code: string,
title: string
): Promise<void> {
const meta = await client.findOne(documents.class.DocumentMeta, { _id })
if (meta !== undefined) {
await client.update(meta, { title: `${code} ${title}` })
}
}
+1 -1
View File
@@ -166,7 +166,7 @@ export async function createControlledDocMetadata (
space,
{
documents: 0,
title: `${prefix}-${seqNumber} ${specTitle}`
title: `${specCode} ${specTitle}`
},
metaId
)
+17 -8
View File
@@ -156,17 +156,25 @@ export function isFolder (doc: ProjectDocument | undefined): boolean {
return doc !== undefined && doc.document === documents.ids.Folder
}
function getDocumentSortSequence (doc: ControlledDocument | undefined): number[] {
return doc !== undefined ? [doc.seqNumber, doc.major, doc.minor, doc.createdOn ?? 0] : [0, 0, 0, 0]
}
export function compareDocumentVersions (
doc1: ControlledDocument | undefined,
doc2: ControlledDocument | undefined
): number {
const s0 = getDocumentSortSequence(doc1)
const s1 = getDocumentSortSequence(doc2)
return s0.reduce((r, v, i) => (r !== 0 ? r : s1[i] - v), 0)
}
function extractPresentableStateFromDocumentBundle (bundle: DocumentBundle, prjmeta: ProjectMeta): DocumentBundle {
bundle = { ...bundle }
const person = getCurrentAccount().person as Ref<Employee>
const documentById = toIdMap(bundle.ControlledDocument)
const getSortSequence = (prjdoc: ProjectDocument): number[] => {
const doc = documentById.get(prjdoc.document as Ref<ControlledDocument>)
return doc !== undefined ? [doc.seqNumber, doc.major, doc.minor, doc.createdOn ?? 0] : [0, 0, 0, 0]
}
const prjdoc = bundle.ProjectDocument.filter((prjdoc) => {
if (prjdoc.attachedTo !== prjmeta._id) return false
if (isFolder(prjdoc)) return true
@@ -174,9 +182,10 @@ function extractPresentableStateFromDocumentBundle (bundle: DocumentBundle, prjm
const isPublicState = doc?.state === DocumentState.Effective || doc?.state === DocumentState.Archived
return doc !== undefined && (isPublicState || isCollaborator(doc, person))
}).sort((a, b) => {
const s0 = getSortSequence(a)
const s1 = getSortSequence(b)
return s0.reduce((r, v, i) => (r !== 0 ? r : s1[i] - v), 0)
return compareDocumentVersions(
documentById.get(a.document as Ref<ControlledDocument>),
documentById.get(b.document as Ref<ControlledDocument>)
)
})[0]
const doc = prjdoc !== undefined ? documentById.get(prjdoc.document as Ref<ControlledDocument>) : undefined
@@ -18,7 +18,7 @@
import presentation, { SearchResult, reduceCalls, searchFor, type SearchItem } from '@hcengineering/presentation'
import { Label, ListView, resizeObserver } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getReferenceLabel } from './extension/reference'
import { getReferenceLabel, getReferenceObject } from './extension/reference'
export let query: string = ''
@@ -31,11 +31,12 @@
let selection = 0
async function handleSelectItem (item: SearchResultDoc): Promise<void> {
const label = await getReferenceLabel(item.doc._class, item.doc._id)
const obj = (await getReferenceObject(item.doc._class, item.doc._id)) ?? item.doc
const label = await getReferenceLabel(obj._class, obj._id)
dispatch('close', {
id: item.id,
id: obj._id,
label,
objectclass: item.doc._class
objectclass: obj._class
})
}
@@ -344,10 +344,30 @@ export async function getReferenceLabel<T extends Doc> (
return label
}
export async function getReferenceObject<T extends Doc> (
objectclass: Ref<Class<T>>,
id: Ref<T>,
doc?: T
): Promise<Doc | undefined> {
const client = getClient()
const hierarchy = client.getHierarchy()
const referenceObjectProvider = hierarchy.classHierarchyMixin(
objectclass as Ref<Class<Doc>>,
view.mixin.ReferenceObjectProvider
)
const referenceObjectProviderFn =
referenceObjectProvider !== undefined ? await getResource(referenceObjectProvider.provider) : undefined
return await referenceObjectProviderFn?.(client, id, doc)
}
export async function getReferenceFromUrl (urlString: string): Promise<ReferenceNodeProps | undefined> {
const target = await getTargetObjectFromUrl(urlString)
let target = await getTargetObjectFromUrl(urlString)
if (target === undefined) return
target = (await getReferenceObject(target._class, target._id)) ?? target
const label = await getReferenceLabel(target._class, target._id)
if (label === '') return
+2
View File
@@ -52,6 +52,7 @@ import {
ObjectTitle,
ObjectTooltip,
ObjectValidator,
ReferenceObjectProvider,
AttrPresenter,
PreviewPresenter,
SpaceHeader,
@@ -96,6 +97,7 @@ const view = plugin(viewId, {
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>,
ObjectTitle: '' as Ref<Mixin<ObjectTitle>>,
ObjectIdentifier: '' as Ref<Mixin<ObjectIdentifier>>,
ReferenceObjectProvider: '' as Ref<Mixin<ReferenceObjectProvider>>,
ObjectTooltip: '' as Ref<Mixin<ObjectTooltip>>,
SpaceHeader: '' as Ref<Mixin<SpaceHeader>>,
SpaceName: '' as Ref<Mixin<SpaceName>>,
+7
View File
@@ -290,6 +290,13 @@ export interface ObjectIdentifier extends Class<Doc> {
provider: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>
}
/**
* @public
*/
export interface ReferenceObjectProvider extends Class<Doc> {
provider: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<Doc | undefined>>
}
/**
* @public
*/
@@ -9,31 +9,30 @@ import documents, {
DocumentApprovalRequest,
DocumentState,
DocumentTemplate,
getDocumentId,
getEffectiveDocUpdate,
type DocumentRequest,
type DocumentTraining
} from '@hcengineering/controlled-documents'
import core, {
AccountRole,
combineAttributes,
Doc,
DocumentQuery,
Ref,
SortingOrder,
Tx,
TxCreateDoc,
TxCUD,
TxFactory,
TxUpdateDoc,
type Account,
type RolesAssignment,
type Timestamp,
Doc,
combineAttributes,
TxCUD
type Timestamp
} from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import { RequestStatus } from '@hcengineering/request'
import { TriggerControl } from '@hcengineering/server-core'
import training, { TrainingState, type TrainingRequest } from '@hcengineering/training'
import { NotificationType } from '@hcengineering/notification'
async function getDocs (
control: TriggerControl,
@@ -77,7 +76,7 @@ function archiveDocs (docs: ControlledDocument[], txFactory: TxFactory): Tx[] {
function updateMeta (doc: ControlledDocument, txFactory: TxFactory): Tx[] {
return [
txFactory.createTxUpdateDoc(doc.attachedToClass, doc.space, doc.attachedTo, {
title: `${getDocumentId(doc)} ${doc.title}`
title: `${doc.code} ${doc.title}`
})
]
}