UBERF-7356 Drive file versions (#6049)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-07-12 14:11:21 +07:00 committed by GitHub
parent f9962fe086
commit d2706ffed8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 880 additions and 140 deletions

View File

@ -42,6 +42,7 @@ import { activityOperation } from '@hcengineering/model-activity'
import { activityServerOperation } from '@hcengineering/model-server-activity' import { activityServerOperation } from '@hcengineering/model-server-activity'
import { loveId, loveOperation } from '@hcengineering/model-love' import { loveId, loveOperation } from '@hcengineering/model-love'
import { documentOperation } from '@hcengineering/model-document' import { documentOperation } from '@hcengineering/model-document'
import { driveOperation } from '@hcengineering/model-drive'
import { textEditorOperation } from '@hcengineering/model-text-editor' import { textEditorOperation } from '@hcengineering/model-text-editor'
import { questionsOperation } from '@hcengineering/model-questions' import { questionsOperation } from '@hcengineering/model-questions'
import { trainingOperation } from '@hcengineering/model-training' import { trainingOperation } from '@hcengineering/model-training'
@ -79,6 +80,7 @@ export const migrateOperations: [string, MigrateOperation][] = [
['activityServer', activityServerOperation], ['activityServer', activityServerOperation],
[loveId, loveOperation], [loveId, loveOperation],
['document', documentOperation], ['document', documentOperation],
['drive', driveOperation],
['textEditor', textEditorOperation], ['textEditor', textEditorOperation],
// We should call it after activityServer and chunter // We should call it after activityServer and chunter
['notification', notificationOperation] ['notification', notificationOperation]

View File

@ -17,45 +17,62 @@ import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import core, { import core, {
type Blob, type Blob,
type Class,
type CollectionSize,
type Domain, type Domain,
type FindOptions, type FindOptions,
type Ref,
type Role, type Role,
type RolesAssignment, type RolesAssignment,
Account, Account,
AccountRole, AccountRole,
IndexKind, IndexKind,
Ref,
SortingOrder SortingOrder
} from '@hcengineering/core' } from '@hcengineering/core'
import { type Drive, type File, type Folder, type Resource, driveId } from '@hcengineering/drive' import {
type Drive,
type File,
type FileVersion,
type Folder,
type Resource,
TypeFileVersion,
driveId
} from '@hcengineering/drive'
import { import {
type Builder, type Builder,
Collection,
Hidden, Hidden,
Index, Index,
Mixin, Mixin,
Model, Model,
Prop, Prop,
ReadOnly, ReadOnly,
TypeFileSize,
TypeRecord, TypeRecord,
TypeRef, TypeRef,
TypeString, TypeString,
UX, TypeTimestamp,
Collection UX
} from '@hcengineering/model' } from '@hcengineering/model'
import { TDoc, TTypedSpace } from '@hcengineering/model-core' import { TAttachedDoc, TDoc, TType, TTypedSpace } from '@hcengineering/model-core'
import print from '@hcengineering/model-print' import print from '@hcengineering/model-print'
import tracker from '@hcengineering/model-tracker' import tracker from '@hcengineering/model-tracker'
import view, { type Viewlet, actionTemplates, createAction } from '@hcengineering/model-view' import view, { type Viewlet, actionTemplates, classPresenter, createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
import drive from './plugin' import drive from './plugin'
export { driveId } from '@hcengineering/drive' export { driveId } from '@hcengineering/drive'
export { driveOperation } from './migration'
export { drive as default } export { drive as default }
export const DOMAIN_DRIVE = 'drive' as Domain export const DOMAIN_DRIVE = 'drive' as Domain
@Model(drive.class.TypeFileVersion, core.class.Type)
@UX(core.string.Number)
export class TTypeFileVersion extends TType {}
@Model(drive.class.Drive, core.class.TypedSpace) @Model(drive.class.Drive, core.class.TypedSpace)
@UX(drive.string.Drive) @UX(drive.string.Drive)
export class TDrive extends TTypedSpace implements Drive {} export class TDrive extends TTypedSpace implements Drive {}
@ -75,15 +92,6 @@ export class TResource extends TDoc implements Resource {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
name!: string name!: string
@Prop(TypeRef(core.class.Blob), drive.string.File)
@ReadOnly()
file?: Ref<Blob>
@Prop(TypeRef(core.class.Blob), drive.string.Preview)
@ReadOnly()
@Hidden()
preview?: Ref<Blob>
@Prop(TypeRef(drive.class.Resource), drive.string.Parent) @Prop(TypeRef(drive.class.Resource), drive.string.Parent)
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
@ReadOnly() @ReadOnly()
@ -95,6 +103,10 @@ export class TResource extends TDoc implements Resource {
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments) @Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number comments?: number
@Prop(TypeRef(drive.class.FileVersion), drive.string.Version)
@ReadOnly()
file?: Ref<FileVersion>
} }
@Model(drive.class.Folder, drive.class.Resource, DOMAIN_DRIVE) @Model(drive.class.Folder, drive.class.Resource, DOMAIN_DRIVE)
@ -110,20 +122,11 @@ export class TFolder extends TResource implements Folder {
declare path: Ref<Folder>[] declare path: Ref<Folder>[]
declare file: undefined declare file: undefined
declare preview: undefined
} }
@Model(drive.class.File, drive.class.Resource, DOMAIN_DRIVE) @Model(drive.class.File, drive.class.Resource, DOMAIN_DRIVE)
@UX(drive.string.File) @UX(drive.string.File)
export class TFile extends TResource implements File { export class TFile extends TResource implements File {
@Prop(TypeRef(core.class.Blob), drive.string.File)
@ReadOnly()
declare file: Ref<Blob>
@Prop(TypeRecord(), drive.string.Metadata)
@ReadOnly()
metadata?: Record<string, any>
@Prop(TypeRef(drive.class.Folder), drive.string.Parent) @Prop(TypeRef(drive.class.Folder), drive.string.Parent)
@Index(IndexKind.Indexed) @Index(IndexKind.Indexed)
@ReadOnly() @ReadOnly()
@ -132,6 +135,75 @@ export class TFile extends TResource implements File {
@Prop(TypeRef(drive.class.Folder), drive.string.Path) @Prop(TypeRef(drive.class.Folder), drive.string.Path)
@ReadOnly() @ReadOnly()
declare path: Ref<Folder>[] declare path: Ref<Folder>[]
@Prop(TypeRef(drive.class.FileVersion), drive.string.Version)
@ReadOnly()
declare file: Ref<FileVersion>
@Prop(Collection(drive.class.FileVersion), drive.string.FileVersion)
@ReadOnly()
versions!: CollectionSize<FileVersion>
@Prop(TypeFileVersion(), drive.string.Version)
@ReadOnly()
version!: number
}
@Model(drive.class.FileVersion, core.class.AttachedDoc, DOMAIN_DRIVE)
@UX(drive.string.FileVersion)
export class TFileVersion extends TAttachedDoc implements FileVersion {
declare space: Ref<Drive>
@Prop(TypeRef(drive.class.File), core.string.AttachedTo)
@Index(IndexKind.Indexed)
declare attachedTo: Ref<File>
@Prop(TypeRef(core.class.Class), core.string.AttachedToClass)
@Index(IndexKind.Indexed)
declare attachedToClass: Ref<Class<File>>
@Prop(TypeString(), core.string.Collection)
@Hidden()
override collection: 'versions' = 'versions'
@Prop(TypeString(), drive.string.Name)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeRef(core.class.Blob), drive.string.File)
@ReadOnly()
file!: Ref<Blob>
@Prop(TypeFileSize(), drive.string.Size)
@ReadOnly()
size!: number
@Prop(TypeString(), drive.string.ContentType)
@ReadOnly()
type!: string
@Prop(TypeTimestamp(), drive.string.LastModified)
@ReadOnly()
lastModified!: number
@Prop(TypeRecord(), drive.string.Metadata)
@ReadOnly()
metadata?: Record<string, any>
@Prop(TypeFileVersion(), drive.string.Version)
@ReadOnly()
version!: number
}
function defineTypes (builder: Builder): void {
builder.createModel(TTypeFileVersion)
classPresenter(
builder,
drive.class.TypeFileVersion,
drive.component.FileVersionVersionPresenter,
drive.component.FileVersionVersionPresenter
)
} }
function defineDrive (builder: Builder): void { function defineDrive (builder: Builder): void {
@ -266,21 +338,20 @@ function defineResource (builder: Builder): void {
}, },
'$lookup.file.size', '$lookup.file.size',
'comments', 'comments',
'$lookup.file.modifiedOn', '$lookup.file.lastModified',
'createdBy' 'createdBy'
], ],
/* eslint-disable @typescript-eslint/consistent-type-assertions */ /* eslint-disable @typescript-eslint/consistent-type-assertions */
options: { options: {
lookup: { lookup: {
file: core.class.Blob, file: drive.class.FileVersion
preview: core.class.Blob
}, },
sort: { sort: {
_class: SortingOrder.Descending _class: SortingOrder.Descending
} }
} as FindOptions<Resource>, } as FindOptions<Resource>,
configOptions: { configOptions: {
hiddenKeys: ['name', 'file', 'parent', 'path', 'type'], hiddenKeys: ['name', 'parent', 'path', 'file', 'versions'],
sortable: true sortable: true
} }
}, },
@ -325,14 +396,13 @@ function defineResource (builder: Builder): void {
'createdBy' 'createdBy'
], ],
configOptions: { configOptions: {
hiddenKeys: ['name', 'file', 'parent', 'path'], hiddenKeys: ['name', 'parent', 'path', 'file', 'versions'],
sortable: true sortable: true
}, },
/* eslint-disable @typescript-eslint/consistent-type-assertions */ /* eslint-disable @typescript-eslint/consistent-type-assertions */
options: { options: {
lookup: { lookup: {
file: core.class.Blob, file: drive.class.FileVersion
preview: core.class.Blob
}, },
sort: { sort: {
_class: SortingOrder.Descending _class: SortingOrder.Descending
@ -429,6 +499,45 @@ function defineFolder (builder: Builder): void {
}) })
} }
function defineFileVersion (builder: Builder): void {
builder.createModel(TFileVersion)
builder.mixin(drive.class.FileVersion, core.class.Class, view.mixin.ObjectPresenter, {
presenter: drive.component.FileVersionPresenter
})
// Actions
builder.mixin(drive.class.FileVersion, core.class.Class, view.mixin.IgnoreActions, {
actions: [
view.action.Open,
view.action.OpenInNewTab,
view.action.Delete,
print.action.Print,
tracker.action.EditRelatedTargets,
tracker.action.NewRelatedIssue
]
})
createAction(
builder,
{
action: drive.actionImpl.RestoreFileVersion,
label: drive.string.Restore,
icon: drive.icon.Restore,
category: drive.category.Drive,
input: 'none',
target: drive.class.FileVersion,
context: {
mode: ['context', 'browser'],
application: drive.app.Drive,
group: 'edit'
}
},
drive.action.RestoreFileVersion
)
}
function defineFile (builder: Builder): void { function defineFile (builder: Builder): void {
builder.createModel(TFile) builder.createModel(TFile)
@ -506,6 +615,24 @@ function defineFile (builder: Builder): void {
drive.action.RenameFile drive.action.RenameFile
) )
// createAction(
// builder,
// {
// action: drive.actionImpl.UploadFile,
// label: drive.string.UploadFile,
// icon: drive.icon.File,
// category: drive.category.Drive,
// input: 'focus',
// target: drive.class.File,
// context: {
// mode: ['context', 'browser'],
// application: drive.app.Drive,
// group: 'tools'
// }
// },
// drive.action.UploadFile
// )
createAction(builder, { createAction(builder, {
...actionTemplates.move, ...actionTemplates.move,
action: view.actionImpl.ShowPopup, action: view.actionImpl.ShowPopup,
@ -580,9 +707,11 @@ export function createModel (builder: Builder): void {
drive.viewlet.Grid drive.viewlet.Grid
) )
defineTypes(builder)
defineDrive(builder) defineDrive(builder)
defineResource(builder) defineResource(builder)
defineFolder(builder) defineFolder(builder)
defineFile(builder) defineFile(builder)
defineFileVersion(builder)
defineApplication(builder) defineApplication(builder)
} }

View File

@ -0,0 +1,102 @@
//
// Copyright © 2022-2024 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 core, { type Blob, type Ref, DOMAIN_BLOB, generateId, toIdMap } from '@hcengineering/core'
import type { File, FileVersion } from '@hcengineering/drive'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
tryMigrate,
tryUpgrade
} from '@hcengineering/model'
import drive, { DOMAIN_DRIVE, driveId } from './index'
async function migrateFileVersions (client: MigrationClient): Promise<void> {
type ExFile = Omit<File, 'file'> & {
file: Ref<Blob>
metadata?: Record<string, any>
}
const files = await client.find<File>(DOMAIN_DRIVE, {
_class: drive.class.File,
version: { $exists: false }
})
const blobIds = files.map((p) => (p as unknown as ExFile).file)
const blobs = await client.find<Blob>(DOMAIN_BLOB, { _id: { $in: blobIds }, _class: core.class.Blob })
const blobsById = toIdMap(blobs)
for (const file of files) {
const exfile = file as unknown as ExFile
const blob = blobsById.get(exfile.file)
if (blob === undefined) continue
const fileVersionId: Ref<FileVersion> = generateId()
await client.create<FileVersion>(DOMAIN_DRIVE, {
_id: fileVersionId,
_class: drive.class.FileVersion,
attachedTo: file._id,
attachedToClass: file._class,
collection: 'versions',
modifiedOn: file.modifiedOn,
modifiedBy: file.modifiedBy,
space: file.space,
name: exfile.name,
file: blob._id,
size: blob.size,
lastModified: blob.modifiedOn,
type: blob.contentType,
metadata: exfile.metadata,
version: 1
})
await client.update<File>(
DOMAIN_DRIVE,
{
_id: file._id,
_class: file._class
},
{
$set: {
version: 1,
versions: 1,
file: fileVersionId
},
$unset: {
metadata: 1
}
}
)
}
}
export const driveOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, driveId, [
{
state: 'file-versions',
func: migrateFileVersions
}
])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {
await tryUpgrade(state, client, driveId, [])
}
}

View File

@ -43,6 +43,8 @@ export default mergeIds(driveId, drive, {
FolderPresenter: '' as AnyComponent, FolderPresenter: '' as AnyComponent,
FilePresenter: '' as AnyComponent, FilePresenter: '' as AnyComponent,
FileSizePresenter: '' as AnyComponent, FileSizePresenter: '' as AnyComponent,
FileVersionPresenter: '' as AnyComponent,
FileVersionVersionPresenter: '' as AnyComponent,
MoveResource: '' as AnyComponent, MoveResource: '' as AnyComponent,
ResourcePresenter: '' as AnyComponent ResourcePresenter: '' as AnyComponent
}, },
@ -68,7 +70,8 @@ export default mergeIds(driveId, drive, {
EditDrive: '' as Ref<Action>, EditDrive: '' as Ref<Action>,
DownloadFile: '' as Ref<Action>, DownloadFile: '' as Ref<Action>,
RenameFile: '' as Ref<Action>, RenameFile: '' as Ref<Action>,
RenameFolder: '' as Ref<Action> RenameFolder: '' as Ref<Action>,
RestoreFileVersion: '' as Ref<Action>
}, },
actionImpl: { actionImpl: {
CreateChildFolder: '' as ViewAction, CreateChildFolder: '' as ViewAction,
@ -76,17 +79,21 @@ export default mergeIds(driveId, drive, {
EditDrive: '' as ViewAction, EditDrive: '' as ViewAction,
DownloadFile: '' as ViewAction, DownloadFile: '' as ViewAction,
RenameFile: '' as ViewAction, RenameFile: '' as ViewAction,
RenameFolder: '' as ViewAction RenameFolder: '' as ViewAction,
RestoreFileVersion: '' as ViewAction
}, },
string: { string: {
Grid: '' as IntlString, Grid: '' as IntlString,
Name: '' as IntlString, Name: '' as IntlString,
Description: '' as IntlString, Description: '' as IntlString,
Metadata: '' as IntlString, Metadata: '' as IntlString,
ContentType: '' as IntlString,
Size: '' as IntlString,
LastModified: '' as IntlString,
Parent: '' as IntlString, Parent: '' as IntlString,
Path: '' as IntlString, Path: '' as IntlString,
Drives: '' as IntlString, Drives: '' as IntlString,
Download: '' as IntlString, Version: '' as IntlString,
Preview: '' as IntlString Restore: '' as IntlString
} }
}) })

View File

@ -24,10 +24,10 @@ export { serverDriveId } from '@hcengineering/server-drive'
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverDrive.trigger.OnFileDelete, trigger: serverDrive.trigger.OnFileVersionDelete,
txMatch: { txMatch: {
_class: core.class.TxRemoveDoc, _class: core.class.TxRemoveDoc,
objectClass: drive.class.File objectClass: drive.class.FileVersion
} }
}) })

View File

@ -24,5 +24,7 @@
<path d="M18.7,9.6C18.5,6,15.6,3.2,12,3.2C8.4,3.2,5.5,6,5.3,9.5c-2.3,0.7-4,2.9-4,5.5c0,3.2,2.6,5.8,5.8,5.8 c0.5,0,1.1-0.1,1.6-0.2C9,20.4,9.2,20,9.1,19.6S8.6,19,8.2,19.1c-0.4,0.1-0.8,0.2-1.2,0.2c-2.3,0-4.2-1.9-4.2-4.2 c0-2,1.4-3.7,3.3-4.1c0.2,0,0.3-0.1,0.4-0.2c0.2-0.1,0.4-0.4,0.4-0.6c0-2.9,2.4-5.2,5.2-5.2s5.2,2.4,5.2,5.2c0,0.1,0,0.1,0,0.2 c0,0.3,0.2,0.6,0.6,0.7c1.9,0.4,3.3,2.1,3.3,4.1c0,2.3-1.9,4.2-4.2,4.2c-0.4,0-0.8-0.1-1.2-0.2c-0.4-0.1-0.8,0.1-0.9,0.5 c-0.1,0.4,0.1,0.8,0.5,0.9c0.5,0.1,1,0.2,1.6,0.2c3.2,0,5.8-2.6,5.8-5.8C22.6,12.5,21,10.4,18.7,9.6z" /> <path d="M18.7,9.6C18.5,6,15.6,3.2,12,3.2C8.4,3.2,5.5,6,5.3,9.5c-2.3,0.7-4,2.9-4,5.5c0,3.2,2.6,5.8,5.8,5.8 c0.5,0,1.1-0.1,1.6-0.2C9,20.4,9.2,20,9.1,19.6S8.6,19,8.2,19.1c-0.4,0.1-0.8,0.2-1.2,0.2c-2.3,0-4.2-1.9-4.2-4.2 c0-2,1.4-3.7,3.3-4.1c0.2,0,0.3-0.1,0.4-0.2c0.2-0.1,0.4-0.4,0.4-0.6c0-2.9,2.4-5.2,5.2-5.2s5.2,2.4,5.2,5.2c0,0.1,0,0.1,0,0.2 c0,0.3,0.2,0.6,0.6,0.7c1.9,0.4,3.3,2.1,3.3,4.1c0,2.3-1.9,4.2-4.2,4.2c-0.4,0-0.8-0.1-1.2-0.2c-0.4-0.1-0.8,0.1-0.9,0.5 c-0.1,0.4,0.1,0.8,0.5,0.9c0.5,0.1,1,0.2,1.6,0.2c3.2,0,5.8-2.6,5.8-5.8C22.6,12.5,21,10.4,18.7,9.6z" />
<path d="M16.1,14.5c-0.3-0.3-0.8-0.3-1.1,0l-2.3,2.3V10c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8v6.7l-2.2-2.2 c-0.3-0.3-0.8-0.3-1.1,0c-0.3,0.3-0.3,0.8,0,1.1l3.5,3.5c0.3,0.3,0.8,0.3,1.1,0l3.6-3.5C16.4,15.2,16.4,14.8,16.1,14.5z" /> <path d="M16.1,14.5c-0.3-0.3-0.8-0.3-1.1,0l-2.3,2.3V10c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8v6.7l-2.2-2.2 c-0.3-0.3-0.8-0.3-1.1,0c-0.3,0.3-0.3,0.8,0,1.1l3.5,3.5c0.3,0.3,0.8,0.3,1.1,0l3.6-3.5C16.4,15.2,16.4,14.8,16.1,14.5z" />
</symbol> </symbol>
<symbol id="restore" viewBox="0 0 32 32">
<path d="M12 10H24.1851L20.5977 6.4141L22 5L28 11L22 17L20.5977 15.5854L24.1821 12H12C10.4087 12 8.88258 12.6321 7.75736 13.7574C6.63214 14.8826 6 16.4087 6 18C6 19.5913 6.63214 21.1174 7.75736 22.2426C8.88258 23.3679 10.4087 24 12 24H19C19.5523 24 20 24.4477 20 25C20 25.5523 19.5523 26 19 26H12C9.87827 26 7.84344 25.1571 6.34315 23.6569C4.84285 22.1566 4 20.1217 4 18C4 15.8783 4.84285 13.8434 6.34315 12.3431C7.84344 10.8429 9.87827 10 12 10Z" />
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -4,19 +4,27 @@
"Drives": "Drives", "Drives": "Drives",
"Grid": "Grid", "Grid": "Grid",
"File": "File", "File": "File",
"FileVersion": "File version",
"FileVersions": "File versions",
"Folder": "Folder", "Folder": "Folder",
"Resource": "Resource", "Resource": "Resource",
"Name": "Name", "Name": "Name",
"Description": "Description", "Description": "Description",
"Parent": "Parent", "Parent": "Parent",
"Path": "Path", "Path": "Path",
"Version": "Version",
"Size": "Size",
"ContentType": "Content type",
"LastModified": "Last Modified",
"Download": "Download", "Download": "Download",
"Upload": "Upload",
"CreateDrive": "Create Drive", "CreateDrive": "Create Drive",
"CreateFolder": "Create Folder", "CreateFolder": "Create Folder",
"UploadFile": "Upload File", "UploadFile": "Upload File",
"UploadFolder": "Upload Folder", "UploadFolder": "Upload Folder",
"EditDrive": "Edit Drive", "EditDrive": "Edit Drive",
"Rename": "Rename", "Rename": "Rename",
"Restore": "Restore",
"RoleLabel": "Role", "RoleLabel": "Role",
"Root": "/" "Root": "/"
} }

View File

@ -4,19 +4,27 @@
"Drives": "Unidades", "Drives": "Unidades",
"Grid": "Red", "Grid": "Red",
"File": "Archivo", "File": "Archivo",
"FileVersion": "Versión del archivo",
"FileVersions": "Versiones de archivos",
"Folder": "Carpeta", "Folder": "Carpeta",
"Resource": "Recurso", "Resource": "Recurso",
"Name": "Nombre", "Name": "Nombre",
"Description": "Descripción", "Description": "Descripción",
"Parent": "Padre", "Parent": "Padre",
"Path": "Ruta", "Path": "Ruta",
"Version": "Versión",
"Size": "Tamaño",
"ContentType": "Tipo de contenido",
"LastModified": "Última modificación",
"Download": "Descargar", "Download": "Descargar",
"Upload": "Subir",
"CreateDrive": "Crear unidad", "CreateDrive": "Crear unidad",
"CreateFolder": "Crear carpeta", "CreateFolder": "Crear carpeta",
"UploadFile": "Subir archivo", "UploadFile": "Subir archivo",
"UploadFolder": "Subir carpeta", "UploadFolder": "Subir carpeta",
"EditDrive": "Editar unidad", "EditDrive": "Editar unidad",
"Rename": "Renombrar", "Rename": "Renombrar",
"Restore": "Restaurar",
"RoleLabel": "Rol", "RoleLabel": "Rol",
"Root": "/" "Root": "/"
} }

View File

@ -4,22 +4,27 @@
"Drives": "Disques", "Drives": "Disques",
"Grid": "Grille", "Grid": "Grille",
"File": "Fichier", "File": "Fichier",
"FileVersion": "Version du fichier",
"FileVersions": "Versions de fichiers",
"Folder": "Dossier", "Folder": "Dossier",
"Resource": "Ressource", "Resource": "Ressource",
"Name": "Nom", "Name": "Nom",
"Description": "Description", "Description": "Description",
"Size": "Taille",
"Type": "Type",
"LastModified": "Dernière modification",
"Parent": "Parent", "Parent": "Parent",
"Path": "Chemin", "Path": "Chemin",
"Version": "Version",
"Size": "Taille",
"ContentType": "Type",
"LastModified": "Dernière modification",
"Download": "Télécharger", "Download": "Télécharger",
"Upload": "Téléverser",
"CreateDrive": "Créer un disque", "CreateDrive": "Créer un disque",
"CreateFolder": "Créer un dossier", "CreateFolder": "Créer un dossier",
"UploadFile": "Télécharger un fichier", "UploadFile": "Télécharger un fichier",
"UploadFolder": "Télécharger un dossier", "UploadFolder": "Télécharger un dossier",
"EditDrive": "Modifier le disque", "EditDrive": "Modifier le disque",
"Rename": "Renommer", "Rename": "Renommer",
"Restore": "Restaurer",
"RoleLabel": "Rôle", "RoleLabel": "Rôle",
"Root": "/" "Root": "/"
} }

View File

@ -4,19 +4,27 @@
"Drives": "Unidades", "Drives": "Unidades",
"Grid": "Grade", "Grid": "Grade",
"File": "Ficheiro", "File": "Ficheiro",
"FileVersion": "Versão do ficheiro",
"FileVersions": "Versões de ficheiro",
"Folder": "Pasta", "Folder": "Pasta",
"Resource": "Recurso", "Resource": "Recurso",
"Name": "Nome", "Name": "Nome",
"Description": "Descrição", "Description": "Descrição",
"Parent": "Pai", "Parent": "Pai",
"Path": "Caminho", "Path": "Caminho",
"Version": "Versão",
"Size": "Tamanho",
"ContentType": "Tipo de conteúdo",
"LastModified": "Última modificação",
"Download": "Descarregar", "Download": "Descarregar",
"Upload": "Carregar",
"CreateDrive": "Criar unidade", "CreateDrive": "Criar unidade",
"CreateFolder": "Criar pasta", "CreateFolder": "Criar pasta",
"UploadFile": "Carregar ficheiro", "UploadFile": "Carregar ficheiro",
"UploadFolder": "Carregar pasta", "UploadFolder": "Carregar pasta",
"EditDrive": "Editar unidade", "EditDrive": "Editar unidade",
"Rename": "Renomear", "Rename": "Renomear",
"Restore": "Restaurar",
"RoleLabel": "Papel", "RoleLabel": "Papel",
"Root": "/" "Root": "/"
} }

View File

@ -4,19 +4,27 @@
"Drives": "Диски", "Drives": "Диски",
"Grid": "Сетка", "Grid": "Сетка",
"File": "Файл", "File": "Файл",
"FileVersion": "Версия файла",
"FileVersions": "Версии файла",
"Folder": "Папка", "Folder": "Папка",
"Resource": "Ресурс", "Resource": "Ресурс",
"Name": "Название", "Name": "Название",
"Description": "Описание", "Description": "Описание",
"Parent": "Родительская папка", "Parent": "Родительская папка",
"Path": "Путь", "Path": "Путь",
"Version": "Версия",
"Size": "Размер",
"ContentType": "Тип содержимого",
"LastModified": "Последнее изменение",
"Download": "Скачать", "Download": "Скачать",
"Upload": "Загрузить",
"CreateDrive": "Создать диск", "CreateDrive": "Создать диск",
"CreateFolder": "Создать папку", "CreateFolder": "Создать папку",
"UploadFile": "Загрузить файл", "UploadFile": "Загрузить файл",
"UploadFolder": "Загрузить папку", "UploadFolder": "Загрузить папку",
"EditDrive": "Редактировать", "EditDrive": "Редактировать",
"Rename": "Переименовать", "Rename": "Переименовать",
"Restore": "Восстановить",
"RoleLabel": "Роль", "RoleLabel": "Роль",
"Root": "/" "Root": "/"
} }

View File

@ -4,19 +4,27 @@
"Drives": "磁盘", "Drives": "磁盘",
"Grid": "网格", "Grid": "网格",
"File": "文件", "File": "文件",
"FileVersion": "檔案版本",
"FileVersions": "檔案版本",
"Folder": "文件夹", "Folder": "文件夹",
"Resource": "资源", "Resource": "资源",
"Name": "名称", "Name": "名称",
"Description": "描述", "Description": "描述",
"Parent": "父级", "Parent": "父级",
"Path": "路径", "Path": "路径",
"Version": "版本",
"Size": "大小",
"ContentType": "内容类型",
"LastModified": "最后修改",
"Download": "下载", "Download": "下载",
"Upload": "上傳",
"CreateDrive": "创建磁盘", "CreateDrive": "创建磁盘",
"CreateFolder": "创建文件夹", "CreateFolder": "创建文件夹",
"UploadFile": "上传文件", "UploadFile": "上传文件",
"UploadFolder": "上传文件夹", "UploadFolder": "上传文件夹",
"EditDrive": "编辑磁盘", "EditDrive": "编辑磁盘",
"Rename": "重命名", "Rename": "重命名",
"Restore": "恢复",
"RoleLabel": "角色", "RoleLabel": "角色",
"Root": "/" "Root": "/"
} }

View File

@ -24,5 +24,6 @@ loadMetadata(drive.icon, {
Folder: `${icons}#folder`, Folder: `${icons}#folder`,
FolderOpen: `${icons}#folder-open`, FolderOpen: `${icons}#folder-open`,
FolderClosed: `${icons}#folder-closed`, FolderClosed: `${icons}#folder-closed`,
Download: `${icons}#download` Download: `${icons}#download`,
Restore: `${icons}#restore`
}) })

View File

@ -20,7 +20,7 @@
import drive from '../plugin' import drive from '../plugin'
import { getFolderIdFromFragment } from '../navigation' import { getFolderIdFromFragment } from '../navigation'
import { createDrive, createFolder, createFiles } from '../utils' import { createDrive, createFolder, uploadFiles } from '../utils'
export let currentSpace: Ref<Drive> | undefined export let currentSpace: Ref<Drive> | undefined
export let currentFragment: string | undefined export let currentFragment: string | undefined
@ -95,7 +95,7 @@
progress = true progress = true
await createFiles(list, currentSpace, parent) await uploadFiles(list, currentSpace, parent)
inputFile.value = '' inputFile.value = ''
progress = false progress = false

View File

@ -13,11 +13,12 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { type Blob } from '@hcengineering/core' import core, { type Blob, type WithLookup } from '@hcengineering/core'
import { type File } from '@hcengineering/drive' import drive, { type File, type FileVersion } from '@hcengineering/drive'
import { FilePreview, createQuery } from '@hcengineering/presentation' import { FilePreview, createQuery } from '@hcengineering/presentation'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import EditFileVersions from './EditFileVersions.svelte'
export let object: File export let object: File
export let readonly: boolean = false export let readonly: boolean = false
@ -26,17 +27,35 @@
const query = createQuery() const query = createQuery()
let blob: Blob | undefined = undefined let blob: Blob | undefined = undefined
$: query.query(core.class.Blob, { _id: object.file }, (res) => { let version: WithLookup<FileVersion> | undefined = undefined
;[blob] = res
}) $: query.query(
drive.class.FileVersion,
{ _id: object.file },
(res) => {
;[version] = res
blob = version?.$lookup?.file
},
{
lookup: {
file: core.class.Blob
}
}
)
onMount(() => { onMount(() => {
dispatch('open', { ignoreKeys: ['file', 'preview', 'parent', 'path', 'metadata'] }) dispatch('open', { ignoreKeys: ['parent', 'path', 'version', 'versions'] })
}) })
</script> </script>
{#if object !== undefined} {#if object !== undefined && version !== undefined}
{#if blob !== undefined} {#if blob !== undefined}
<FilePreview file={blob} name={object.name} metadata={object.metadata} /> <FilePreview file={blob} name={version.name} metadata={version.metadata} />
{/if}
{#if object.versions > 1}
<div class="w-full mt-6">
<EditFileVersions {object} {readonly} />
</div>
{/if} {/if}
{/if} {/if}

View File

@ -0,0 +1,45 @@
<!--
// Copyright © 2024 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 { FindOptions, SortingOrder } from '@hcengineering/core'
import { type File, type FileVersion } from '@hcengineering/drive'
import { Scroller, Section } from '@hcengineering/ui'
import { Table } from '@hcengineering/view-resources'
import drive from '../plugin'
export let object: File
export let readonly: boolean = false
const options: FindOptions<FileVersion> = {
sort: { version: SortingOrder.Descending }
}
</script>
{#if object.versions > 1}
<Section label={drive.string.FileVersions}>
<svelte:fragment slot="content">
<Scroller horizontal>
<Table
_class={drive.class.FileVersion}
config={['version', 'size', 'modifiedOn', 'createdBy']}
query={{ attachedTo: object._id }}
{readonly}
{options}
/>
</Scroller>
</svelte:fragment>
</Section>
{/if}

View File

@ -26,11 +26,7 @@
<DocAttributeBar {object} {readonly} ignoreKeys={[]} /> <DocAttributeBar {object} {readonly} ignoreKeys={[]} />
{#if object.$lookup?.file} {#if object.$lookup?.file}
<DocAttributeBar <DocAttributeBar object={object.$lookup.file} {readonly} ignoreKeys={['name', 'file', 'version', 'version']} />
object={object.$lookup.file}
{readonly}
ignoreKeys={['provider', 'storageId', 'etag', 'version']}
/>
{/if} {/if}
<div class="space-divider bottom" /> <div class="space-divider bottom" />
</Scroller> </Scroller>

View File

@ -16,7 +16,7 @@
import { type Ref } from '@hcengineering/core' import { type Ref } from '@hcengineering/core'
import { type Drive, type Folder } from '@hcengineering/drive' import { type Drive, type Folder } from '@hcengineering/drive'
import { createFiles } from '../utils' import { uploadFiles } from '../utils'
export let space: Ref<Drive> export let space: Ref<Drive>
export let parent: Ref<Folder> export let parent: Ref<Folder>
@ -39,6 +39,9 @@
} }
async function handleDragOver (e: DragEvent): Promise<void> { async function handleDragOver (e: DragEvent): Promise<void> {
if (e.dataTransfer?.files === undefined) {
return
}
if (canDrop !== undefined && !canDrop(e)) { if (canDrop !== undefined && !canDrop(e)) {
return return
} }
@ -63,7 +66,7 @@
// progress = true // progress = true
const list = e.dataTransfer?.files const list = e.dataTransfer?.files
if (list !== undefined && list.length !== 0) { if (list !== undefined && list.length !== 0) {
await createFiles(list, space, parent) await uploadFiles(list, space, parent)
} }
// progress = false // progress = false
} }

View File

@ -33,5 +33,5 @@
<div class="antiHSpacer x2" /> <div class="antiHSpacer x2" />
<DocsNavigator elements={parents} /> <DocsNavigator elements={parents} />
<div class="title"> <div class="title">
<FilePresenter value={object} shouldShowAvatar={false} disabled noUnderline /> <FilePresenter value={object} shouldShowAvatar={false} shouldShowVersion disabled noUnderline />
</div> </div>

View File

@ -0,0 +1,46 @@
<!--
// Copyright © 2024 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 { createEventDispatcher } from 'svelte'
export let multiple: boolean = false
export function upload (): void {
inputFile.click()
}
const dispatch = createEventDispatcher()
let inputFile: HTMLInputElement
async function fileSelected (): Promise<void> {
const files = inputFile.files
if (files === null || files.length === 0) {
return
}
dispatch('selected', files)
}
</script>
<input
bind:this={inputFile}
{multiple}
id="file"
name="file"
type="file"
style="display: none"
disabled={inputFile == null}
on:change={fileSelected}
/>

View File

@ -13,10 +13,10 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { WithLookup, type Ref } from '@hcengineering/core' import { WithLookup, type Ref } from '@hcengineering/core'
import { type File } from '@hcengineering/drive' import { type File, type FileVersion } from '@hcengineering/drive'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import presentation, { IconDownload, createQuery, getBlobHref } from '@hcengineering/presentation' import { createQuery, getBlobHref } from '@hcengineering/presentation'
import { Button, IconMoreH } from '@hcengineering/ui' import { Button, IconMoreH } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { showMenu } from '@hcengineering/view-resources' import { showMenu } from '@hcengineering/view-resources'
@ -24,8 +24,11 @@
import EditFile from './EditFile.svelte' import EditFile from './EditFile.svelte'
import FileAside from './FileAside.svelte' import FileAside from './FileAside.svelte'
import FileHeader from './FileHeader.svelte' import FileHeader from './FileHeader.svelte'
import IconDownload from './icons/FileDownload.svelte'
import IconUpload from './icons/FileUpload.svelte'
import drive from '../plugin' import drive from '../plugin'
import { replaceOneFile } from '../utils'
export let _id: Ref<File> export let _id: Ref<File>
export let readonly: boolean = false export let readonly: boolean = false
@ -37,7 +40,9 @@
} }
let object: WithLookup<File> | undefined = undefined let object: WithLookup<File> | undefined = undefined
let version: FileVersion | undefined = undefined
let download: HTMLAnchorElement let download: HTMLAnchorElement
let upload: HTMLInputElement
const query = createQuery() const query = createQuery()
$: query.query( $: query.query(
@ -45,16 +50,37 @@
{ _id }, { _id },
(res) => { (res) => {
;[object] = res ;[object] = res
version = object?.$lookup?.file
}, },
{ {
lookup: { lookup: {
file: core.class.Blob file: drive.class.FileVersion
} }
} }
) )
function handleDownloadFile (): void {
if (object != null && download != null) {
download.click()
}
}
function handleUploadFile (): void {
if (object != null && upload != null) {
upload.click()
}
}
async function handleFileSelected (): Promise<void> {
const files = upload.files
if (object != null && files !== null && files.length > 0) {
await replaceOneFile(object._id, files[0])
}
upload.value = ''
}
</script> </script>
{#if object} {#if object && version}
<Panel <Panel
{object} {object}
{embedded} {embedded}
@ -66,24 +92,39 @@
on:close on:close
on:update on:update
> >
<input
bind:this={upload}
id="file"
name="file"
type="file"
style="display: none"
disabled={upload == null}
on:change={handleFileSelected}
/>
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<FileHeader {object} /> <FileHeader {object} />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
{#await getBlobHref(object.$lookup?.file, object.file, object.name) then href} {#await getBlobHref(undefined, version.file, object.name) then href}
<a class="no-line" {href} download={object.name} bind:this={download}> <a class="no-line" {href} download={object.name} bind:this={download}>
<Button <Button
icon={IconDownload} icon={IconDownload}
iconProps={{ size: 'medium' }} iconProps={{ size: 'medium' }}
kind={'icon'} kind={'icon'}
showTooltip={{ label: presentation.string.Download }} showTooltip={{ label: drive.string.Download }}
on:click={() => { on:click={handleDownloadFile}
download.click()
}}
/> />
</a> </a>
{/await} {/await}
<Button
icon={IconUpload}
iconProps={{ size: 'medium' }}
kind={'icon'}
showTooltip={{ label: drive.string.Upload }}
on:click={handleUploadFile}
/>
<Button <Button
icon={IconMoreH} icon={IconMoreH}
iconProps={{ size: 'medium' }} iconProps={{ size: 'medium' }}

View File

@ -22,7 +22,7 @@
import { ObjectPresenterType } from '@hcengineering/view' import { ObjectPresenterType } from '@hcengineering/view'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources' import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import { getFileTypeIcon } from '../utils' import { formatFileVersion, getFileTypeIcon } from '../utils'
export let value: WithLookup<File> export let value: WithLookup<File>
export let inline: boolean = false export let inline: boolean = false
@ -32,7 +32,9 @@
export let shouldShowAvatar = true export let shouldShowAvatar = true
export let type: ObjectPresenterType = 'link' export let type: ObjectPresenterType = 'link'
$: icon = getFileTypeIcon(value.$lookup?.file?.contentType ?? '') export let shouldShowVersion = false
$: icon = getFileTypeIcon(value.$lookup?.file?.type ?? '')
</script> </script>
{#if value} {#if value}
@ -46,9 +48,13 @@
<Icon {icon} size={'small'} /> <Icon {icon} size={'small'} />
</div> </div>
{/if} {/if}
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}> <div class="label nowrap flex flex-gap-2" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{value.name} <span>{value.name}</span>
</span> {#if shouldShowVersion}
<span></span>
<span>{formatFileVersion(value.version)}</span>
{/if}
</div>
</div> </div>
</DocNavLink> </DocNavLink>
{:else if type === 'text'} {:else if type === 'text'}

View File

@ -0,0 +1,48 @@
<!--
//
// Copyright © 2024 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 { type FileVersion } from '@hcengineering/drive'
import { ObjectPresenterType } from '@hcengineering/view'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import { formatFileVersion } from '../utils'
export let value: FileVersion
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
export let type: ObjectPresenterType = 'link'
$: version = formatFileVersion(value.version)
</script>
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
{:else if type === 'link'}
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
<div class="flex-presenter">
<div class="label nowrap flex flex-gap-2" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{version}
</div>
</div>
</DocNavLink>
{:else if type === 'text'}
<span class="overflow-label">{version}</span>
{/if}
{/if}

View File

@ -0,0 +1,36 @@
<!--
// Copyright © 2024 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 { Button, ButtonSize } from '@hcengineering/ui'
import { formatFileVersion } from '../utils'
export let value: number | undefined
export let kind: 'no-border' | 'link' | 'list' = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = 'fit-content'
$: version = value ? formatFileVersion(value) : ''
</script>
{#if kind === 'link'}
<Button {kind} {size} {justify} {width}>
<svelte:fragment slot="content">
{version}
</svelte:fragment>
</Button>
{:else}
<span>{version}</span>
{/if}

View File

@ -31,6 +31,8 @@
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: version = object.$lookup?.file
let hovered = false let hovered = false
</script> </script>
@ -39,6 +41,7 @@
class="card-container" class="card-container"
class:selected class:selected
class:hovered class:hovered
draggable="false"
on:mouseover={() => dispatch('obj-focus', object)} on:mouseover={() => dispatch('obj-focus', object)}
on:mouseenter={() => dispatch('obj-focus', object)} on:mouseenter={() => dispatch('obj-focus', object)}
on:focus={() => {}} on:focus={() => {}}
@ -88,11 +91,11 @@
/> />
<span></span> <span></span>
<span class="flex-no-shrink"> <span class="flex-no-shrink">
<TimestampPresenter value={object.$lookup?.file?.modifiedOn ?? object.createdOn ?? object.modifiedOn} /> <TimestampPresenter value={version?.lastModified ?? object.createdOn ?? object.modifiedOn} />
</span> </span>
</div> </div>
<div class="flex-no-shrink font-regular-12"> <div class="flex-no-shrink font-regular-12">
<FileSizePresenter value={object.$lookup?.file?.size} /> <FileSizePresenter value={version?.size} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -35,17 +35,18 @@
let isImage = false let isImage = false
let isError = false let isError = false
$: previewBlob = object.$lookup?.preview ?? object.$lookup?.file $: version = object.$lookup?.file
$: previewRef = object.$lookup?.preview !== undefined ? object.preview : object.file $: previewRef = version?.file
$: isImage = previewBlob?.contentType?.startsWith('image/') ?? false $: isImage = version?.type?.startsWith('image/') ?? false
$: isFolder = hierarchy.isDerived(object._class, drive.class.Folder) $: isFolder = hierarchy.isDerived(object._class, drive.class.Folder)
</script> </script>
{#if isFolder} {#if isFolder}
<Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--theme-trans-color)'} /> <Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--theme-trans-color)'} />
{:else if previewBlob != null && previewRef != null && isImage && !isError} {:else if previewRef != null && isImage && !isError}
{#await getBlobRef(previewBlob, previewRef, object.name, sizeToWidth(size)) then blobSrc} {#await getBlobRef(undefined, previewRef, object.name, sizeToWidth(size)) then blobSrc}
<img <img
draggable="false"
class="img-fit" class="img-fit"
src={blobSrc.src} src={blobSrc.src}
srcset={blobSrc.srcset} srcset={blobSrc.srcset}

View File

@ -0,0 +1,28 @@
<!--
// Copyright © 2024 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">
export let fill: string = 'currentColor'
export let size: 'small' | 'medium' | 'large' = 'small'
</script>
<svg class="svg-{size}" {fill} width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.4999 22.0001H23C22.4477 22.0001 22 21.5523 22 21.0001C22 20.4478 22.4477 20.0001 23 20.0001H23.4999C24.6933 20.0478 25.8569 19.6195 26.7346 18.8093C27.6122 17.9992 28.1321 16.8735 28.1799 15.6801C28.2276 14.4866 27.7993 13.323 26.9891 12.4454C26.179 11.5677 25.0533 11.0478 23.8599 11.0001H22.9999L22.8999 10.1801C22.678 8.49651 21.8517 6.951 20.575 5.83144C19.2982 4.71188 17.658 4.09461 15.9599 4.09461C14.2618 4.09461 12.6215 4.71188 11.3448 5.83144C10.068 6.951 9.24172 8.49651 9.01986 10.1801L8.99986 11.0001H8.13986C6.94638 11.0478 5.82076 11.5677 5.0106 12.4454C4.20044 13.323 3.77212 14.4866 3.81986 15.6801C3.8676 16.8735 4.38749 17.9992 5.26516 18.8093C6.14283 19.6195 7.30638 20.0478 8.49986 20.0001H9C9.55228 20.0001 10 20.4478 10 21.0001C10 21.5523 9.55228 22.0001 9 22.0001H8.49986C6.89626 21.9899 5.35302 21.3873 4.16684 20.3082C2.98066 19.229 2.23528 17.7494 2.07399 16.1539C1.9127 14.5584 2.3469 12.9596 3.29311 11.6649C4.23932 10.3702 5.63074 9.47094 7.19986 9.14006C7.63157 7.12658 8.74069 5.32203 10.3422 4.02753C11.9437 2.73302 13.9406 2.02686 15.9999 2.02686C18.0591 2.02686 20.0561 2.73302 21.6575 4.02753C23.259 5.32203 24.3682 7.12658 24.7999 9.14006C26.369 9.47094 27.7604 10.3702 28.7066 11.6649C29.6528 12.9596 30.087 14.5584 29.9257 16.1539C29.7644 17.7494 29.0191 19.229 27.8329 20.3082C26.6467 21.3873 25.1035 21.9899 23.4999 22.0001Z"
/>
<path
d="M16.9999 26.1701V15.0001C16.9999 14.4478 16.5521 14.0001 15.9999 14.0001C15.4476 14.0001 14.9999 14.4478 14.9999 15.0001V26.1701L12.4099 23.5901L10.9999 25.0001L15.9999 30.0001L20.9999 25.0001L19.5899 23.5901L16.9999 26.1701Z"
/>
</svg>

View File

@ -0,0 +1,28 @@
<!--
// Copyright © 2024 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">
export let fill: string = 'currentColor'
export let size: 'small' | 'medium' | 'large' = 'small'
</script>
<svg class="svg-{size}" {fill} width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.9999 18.0001L12.4099 19.4101L14.9999 16.8301V28.0001C14.9999 28.5523 15.4476 29.0001 15.9999 29.0001C16.5521 29.0001 16.9999 28.5523 16.9999 28.0001V16.8301L19.5899 19.4101L20.9999 18.0001L15.9999 13.0001L10.9999 18.0001Z"
/>
<path
d="M23.4999 22.0001H23C22.4477 22.0001 22 21.5523 22 21.0001C22 20.4478 22.4477 20.0001 23 20.0001H23.4999C24.6933 20.0478 25.8569 19.6195 26.7346 18.8093C27.6122 17.9992 28.1321 16.8735 28.1799 15.6801C28.2276 14.4866 27.7993 13.323 26.9891 12.4454C26.179 11.5677 25.0533 11.0478 23.8599 11.0001H22.9999L22.8999 10.1801C22.678 8.49651 21.8517 6.951 20.575 5.83144C19.2982 4.71188 17.658 4.09461 15.9599 4.09461C14.2618 4.09461 12.6215 4.71188 11.3448 5.83144C10.068 6.951 9.24172 8.49651 9.01986 10.1801L8.99986 11.0001H8.13986C6.94638 11.0478 5.82076 11.5677 5.0106 12.4454C4.20044 13.323 3.77212 14.4866 3.81986 15.6801C3.8676 16.8735 4.38749 17.9992 5.26516 18.8093C6.14283 19.6195 7.30638 20.0478 8.49986 20.0001H9C9.55228 20.0001 10 20.4478 10 21.0001C10 21.5523 9.55228 22.0001 9 22.0001H8.49986C6.89626 21.9899 5.35302 21.3873 4.16684 20.3082C2.98066 19.229 2.23528 17.7494 2.07399 16.1539C1.9127 14.5584 2.3469 12.9596 3.29311 11.6649C4.23932 10.3702 5.63074 9.47094 7.19986 9.14006C7.63157 7.12658 8.74069 5.32203 10.3422 4.02753C11.9437 2.73302 13.9406 2.02686 15.9999 2.02686C18.0591 2.02686 20.0561 2.73302 21.6575 4.02753C23.259 5.32203 24.3682 7.12658 24.7999 9.14006C26.369 9.47094 27.7604 10.3702 28.7066 11.6649C29.6528 12.9596 30.087 14.5584 29.9257 16.1539C29.7644 17.7494 29.0191 19.229 27.8329 20.3082C26.6467 21.3873 25.1035 21.9899 23.4999 22.0001Z"
/>
</svg>

View File

@ -14,7 +14,7 @@
// //
import { type Doc, type Ref, type WithLookup } from '@hcengineering/core' import { type Doc, type Ref, type WithLookup } from '@hcengineering/core'
import drive, { type Drive, type File, type Folder } from '@hcengineering/drive' import drive, { type Drive, type File, type FileVersion, type Folder } from '@hcengineering/drive'
import { type Resources } from '@hcengineering/platform' import { type Resources } from '@hcengineering/platform'
import { getBlobHref } from '@hcengineering/presentation' import { getBlobHref } from '@hcengineering/presentation'
import { showPopup, type Location } from '@hcengineering/ui' import { showPopup, type Location } from '@hcengineering/ui'
@ -29,6 +29,8 @@ import EditFolder from './components/EditFolder.svelte'
import FilePanel from './components/FilePanel.svelte' import FilePanel from './components/FilePanel.svelte'
import FilePresenter from './components/FilePresenter.svelte' import FilePresenter from './components/FilePresenter.svelte'
import FileSizePresenter from './components/FileSizePresenter.svelte' import FileSizePresenter from './components/FileSizePresenter.svelte'
import FileVersionPresenter from './components/FileVersionPresenter.svelte'
import FileVersionVersionPresenter from './components/FileVersionVersionPresenter.svelte'
import FolderPanel from './components/FolderPanel.svelte' import FolderPanel from './components/FolderPanel.svelte'
import FolderPresenter from './components/FolderPresenter.svelte' import FolderPresenter from './components/FolderPresenter.svelte'
import GridView from './components/GridView.svelte' import GridView from './components/GridView.svelte'
@ -36,7 +38,7 @@ import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte' import ResourcePresenter from './components/ResourcePresenter.svelte'
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation' import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
import { createFolder, renameResource } from './utils' import { createFolder, renameResource, restoreFileVersion } from './utils'
async function CreateRootFolder (doc: Drive): Promise<void> { async function CreateRootFolder (doc: Drive): Promise<void> {
await createFolder(doc._id, drive.ids.Root) await createFolder(doc._id, drive.ids.Root)
@ -53,7 +55,9 @@ async function EditDrive (drive: Drive): Promise<void> {
async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> { async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> {
const files = Array.isArray(doc) ? doc : [doc] const files = Array.isArray(doc) ? doc : [doc]
for (const file of files) { for (const file of files) {
const href = await getBlobHref(file.$lookup?.file, file.file, file.name) const version = file.$lookup?.file
if (version != null) {
const href = await getBlobHref(undefined, version.file, version.name)
const link = document.createElement('a') const link = document.createElement('a')
link.style.display = 'none' link.style.display = 'none'
link.target = '_blank' link.target = '_blank'
@ -62,6 +66,7 @@ async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): P
link.click() link.click()
} }
} }
}
async function DriveLinkProvider (doc: Doc): Promise<Location> { async function DriveLinkProvider (doc: Doc): Promise<Location> {
return getDriveLink(doc._id as Ref<Drive>) return getDriveLink(doc._id as Ref<Drive>)
@ -87,6 +92,12 @@ async function RenameFolder (doc: Folder | Folder[]): Promise<void> {
} }
} }
async function RestoreFileVersion (doc: FileVersion | FileVersion[]): Promise<void> {
if (!Array.isArray(doc)) {
await restoreFileVersion(doc)
}
}
export async function CanRenameFile (doc: File | File[] | undefined): Promise<boolean> { export async function CanRenameFile (doc: File | File[] | undefined): Promise<boolean> {
return doc !== undefined && !Array.isArray(doc) return doc !== undefined && !Array.isArray(doc)
} }
@ -107,6 +118,8 @@ export default async (): Promise<Resources> => ({
FilePanel, FilePanel,
FilePresenter, FilePresenter,
FileSizePresenter, FileSizePresenter,
FileVersionPresenter,
FileVersionVersionPresenter,
FolderPanel, FolderPanel,
FolderPresenter, FolderPresenter,
GridView, GridView,
@ -119,7 +132,8 @@ export default async (): Promise<Resources> => ({
EditDrive, EditDrive,
DownloadFile, DownloadFile,
RenameFile, RenameFile,
RenameFolder RenameFolder,
RestoreFileVersion
}, },
function: { function: {
DriveLinkProvider, DriveLinkProvider,

View File

@ -105,11 +105,11 @@ export async function generateFolderLocation (loc: Location, id: Ref<Folder>): P
return { return {
loc: { loc: {
path: [appComponent, workspace, driveId, doc.space], path: [appComponent, workspace, driveId],
fragment: getPanelFragment(doc) fragment: getPanelFragment(doc)
}, },
defaultLocation: { defaultLocation: {
path: [appComponent, workspace, driveId], path: [appComponent, workspace, driveId, doc.space],
fragment: getPanelFragment(doc) fragment: getPanelFragment(doc)
} }
} }
@ -130,11 +130,11 @@ export async function generateFileLocation (loc: Location, id: Ref<File>): Promi
return { return {
loc: { loc: {
path: [appComponent, workspace, driveId, doc.space], path: [appComponent, workspace, driveId],
fragment: getPanelFragment(doc) fragment: getPanelFragment(doc)
}, },
defaultLocation: { defaultLocation: {
path: [appComponent, workspace, driveId], path: [appComponent, workspace, driveId, doc.space],
fragment: getPanelFragment(doc) fragment: getPanelFragment(doc)
} }
} }

View File

@ -23,9 +23,12 @@ export default mergeIds(driveId, drive, {
CreateFolder: '' as IntlString, CreateFolder: '' as IntlString,
UploadFile: '' as IntlString, UploadFile: '' as IntlString,
UploadFolder: '' as IntlString, UploadFolder: '' as IntlString,
Download: '' as IntlString,
Upload: '' as IntlString,
EditDrive: '' as IntlString, EditDrive: '' as IntlString,
Rename: '' as IntlString, Rename: '' as IntlString,
RoleLabel: '' as IntlString, RoleLabel: '' as IntlString,
Root: '' as IntlString Root: '' as IntlString,
FileVersions: '' as IntlString
} }
}) })

View File

@ -13,8 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import { toIdMap, type Class, type Doc, type Ref } from '@hcengineering/core' import { type Class, type Doc, type Ref, toIdMap } from '@hcengineering/core'
import drive, { type Drive, type Folder, type Resource } from '@hcengineering/drive' import type { Drive, File as DriveFile, FileVersion, Folder, Resource } from '@hcengineering/drive'
import drive, { createFile, createFileVersion } from '@hcengineering/drive'
import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform' import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation' import { getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
import { type AnySvelteComponent, showPopup } from '@hcengineering/ui' import { type AnySvelteComponent, showPopup } from '@hcengineering/ui'
@ -38,6 +39,10 @@ async function navigateToDoc (_id: Ref<Doc>, _class: Ref<Class<Doc>>): Promise<v
} }
} }
export function formatFileVersion (version: number): string {
return `v${version}`
}
export async function createFolder (space: Ref<Drive> | undefined, parent: Ref<Folder>, open = false): Promise<void> { export async function createFolder (space: Ref<Drive> | undefined, parent: Ref<Folder>, open = false): Promise<void> {
showPopup(CreateFolder, { space, parent }, 'top', async (id) => { showPopup(CreateFolder, { space, parent }, 'top', async (id) => {
if (open && id !== undefined && id !== null) { if (open && id !== undefined && id !== null) {
@ -58,32 +63,42 @@ export async function editDrive (drive: Drive): Promise<void> {
showPopup(CreateDrive, { drive }) showPopup(CreateDrive, { drive })
} }
export async function createFiles (list: FileList, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> { export async function uploadFiles (list: FileList, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
const client = getClient()
const folder = await client.findOne(drive.class.Folder, { space, _id: parent })
for (let index = 0; index < list.length; index++) { for (let index = 0; index < list.length; index++) {
const file = list.item(index) const file = list.item(index)
if (file !== null) { if (file !== null) {
await createFile(file, space, folder) await uploadOneFile(file, space, parent)
} }
} }
} }
export async function createFile (file: File, space: Ref<Drive>, parent: Folder | undefined): Promise<void> { export async function uploadOneFile (file: File, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
const client = getClient() const client = getClient()
try { try {
const uuid = await uploadFile(file) const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid) const metadata = await getFileMetadata(file, uuid)
await client.createDoc(drive.class.File, space, { const { name, size, type, lastModified } = file
name: file.name, const data = { file: uuid, name, size, type, lastModified, metadata }
file: uuid,
metadata, await createFile(client, space, parent, data)
parent: parent?._id ?? drive.ids.Root, } catch (e) {
path: parent !== undefined ? [parent._id, ...parent.path] : [] void setPlatformStatus(unknownError(e))
}) }
}
export async function replaceOneFile (existing: Ref<DriveFile>, file: File): Promise<void> {
const client = getClient()
try {
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
const { name, size, type, lastModified } = file
const data = { file: uuid, name, size, type, lastModified, metadata }
await createFileVersion(client, existing, data)
} catch (e) { } catch (e) {
void setPlatformStatus(unknownError(e)) void setPlatformStatus(unknownError(e))
} }
@ -130,6 +145,15 @@ export async function moveResources (resources: Resource[], space: Ref<Drive>, p
await ops.commit() await ops.commit()
} }
export async function restoreFileVersion (version: FileVersion): Promise<void> {
const client = getClient()
const file = await client.findOne(drive.class.File, { _id: version.attachedTo })
if (file !== undefined && file.file !== version._id) {
await client.diffUpdate(file, { file: version._id })
}
}
const fileTypesMap: Record<string, AnySvelteComponent> = { const fileTypesMap: Record<string, AnySvelteComponent> = {
'application/pdf': FileTypePdf, 'application/pdf': FileTypePdf,
audio: FileTypeAudio, audio: FileTypeAudio,

View File

@ -16,6 +16,7 @@
import { driveId, drivePlugin } from './plugin' import { driveId, drivePlugin } from './plugin'
export * from './types' export * from './types'
export * from './utils'
export { driveId } export { driveId }
export default drivePlugin export default drivePlugin

View File

@ -17,7 +17,7 @@ import type { Class, Doc, Mixin, Ref, SpaceType, SpaceTypeDescriptor, Type } fro
import type { Asset, IntlString, Plugin, Resource as PlatformResource } from '@hcengineering/platform' import type { Asset, IntlString, Plugin, Resource as PlatformResource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { Location, ResolvedLocation } from '@hcengineering/ui' import type { Location, ResolvedLocation } from '@hcengineering/ui'
import { Drive, File, FileSize, Folder, Resource } from './types' import { Drive, File, FileVersion, Folder, Resource } from './types'
export * from './types' export * from './types'
@ -30,9 +30,10 @@ export const drivePlugin = plugin(driveId, {
class: { class: {
Drive: '' as Ref<Class<Drive>>, Drive: '' as Ref<Class<Drive>>,
File: '' as Ref<Class<File>>, File: '' as Ref<Class<File>>,
FileVersion: '' as Ref<Class<FileVersion>>,
Folder: '' as Ref<Class<Folder>>, Folder: '' as Ref<Class<Folder>>,
Resource: '' as Ref<Class<Resource>>, Resource: '' as Ref<Class<Resource>>,
TypeFileSize: '' as Ref<Class<Type<FileSize>>> TypeFileVersion: '' as Ref<Class<Type<number>>>
}, },
mixin: { mixin: {
DefaultDriveTypeData: '' as Ref<Mixin<Drive>> DefaultDriveTypeData: '' as Ref<Mixin<Drive>>
@ -44,7 +45,8 @@ export const drivePlugin = plugin(driveId, {
Folder: '' as Asset, Folder: '' as Asset,
FolderOpen: '' as Asset, FolderOpen: '' as Asset,
FolderClosed: '' as Asset, FolderClosed: '' as Asset,
Download: '' as Asset Download: '' as Asset,
Restore: '' as Asset
}, },
app: { app: {
Drive: '' as Ref<Doc> Drive: '' as Ref<Doc>
@ -58,6 +60,7 @@ export const drivePlugin = plugin(driveId, {
string: { string: {
Drive: '' as IntlString, Drive: '' as IntlString,
File: '' as IntlString, File: '' as IntlString,
FileVersion: '' as IntlString,
Folder: '' as IntlString, Folder: '' as IntlString,
Resource: '' as IntlString Resource: '' as IntlString
}, },

View File

@ -13,10 +13,14 @@
// limitations under the License. // limitations under the License.
// //
import { Blob, Doc, Ref, TypedSpace } from '@hcengineering/core' import { AttachedDoc, Blob, CollectionSize, Doc, Ref, Type, TypedSpace } from '@hcengineering/core'
import drive from './plugin'
/** @public */ /** @public */
export type FileSize = number export function TypeFileVersion (): Type<number> {
return { _class: drive.class.TypeFileVersion, label: drive.string.FileVersion }
}
/** @public */ /** @public */
export interface Drive extends TypedSpace {} export interface Drive extends TypedSpace {}
@ -24,13 +28,13 @@ export interface Drive extends TypedSpace {}
/** @public */ /** @public */
export interface Resource extends Doc<Drive> { export interface Resource extends Doc<Drive> {
name: string name: string
file?: Ref<Blob>
preview?: Ref<Blob>
parent: Ref<Resource> parent: Ref<Resource>
path: Ref<Resource>[] path: Ref<Resource>[]
comments?: number comments?: number
// ugly but needed here to get version lookup work for Resource
file?: Ref<FileVersion>
} }
/** @public */ /** @public */
@ -43,9 +47,21 @@ export interface Folder extends Resource {
/** @public */ /** @public */
export interface File extends Resource { export interface File extends Resource {
file: Ref<Blob>
metadata?: Record<string, any>
parent: Ref<Folder> parent: Ref<Folder>
path: Ref<Folder>[] path: Ref<Folder>[]
file: Ref<FileVersion>
versions: CollectionSize<FileVersion>
version: number
}
/** @public */
export interface FileVersion extends AttachedDoc<File, 'versions', Drive> {
name: string
file: Ref<Blob>
size: number
type: string
lastModified: number
metadata?: Record<string, any>
version: number
} }

View File

@ -0,0 +1,98 @@
//
// Copyright © 2024 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 AttachedData, type Ref, type TxOperations, generateId } from '@hcengineering/core'
import drive from './plugin'
import type { Drive, File, FileVersion, Folder } from './types'
/** @public */
export async function createFile (
client: TxOperations,
space: Ref<Drive>,
parent: Ref<Folder>,
data: Omit<AttachedData<FileVersion>, 'version'>
): Promise<void> {
const folder = await client.findOne(drive.class.Folder, { _id: parent })
const path = folder !== undefined ? [folder._id, ...folder.path] : []
const version = 1
const versionId: Ref<FileVersion> = generateId()
const fileId = await client.createDoc(drive.class.File, space, {
name: data.name,
parent,
path,
file: versionId,
version,
versions: 0
})
await client.addCollection(
drive.class.FileVersion,
space,
fileId,
drive.class.File,
'versions',
{ ...data, version },
versionId
)
}
/** @public */
export async function createFileVersion (
client: TxOperations,
file: Ref<File>,
data: Omit<AttachedData<FileVersion>, 'version'>
): Promise<void> {
const current = await client.findOne(drive.class.File, { _id: file })
if (current === undefined) {
throw new Error('file not found')
}
const incResult = await client.update(current, { $inc: { version: 1 } }, true)
const version = (incResult as any).object.version
const ops = client.apply(file)
const versionId = await ops.addCollection(
drive.class.FileVersion,
current.space,
current._id,
current._class,
'versions',
{ ...data, version }
)
await ops.update(current, { file: versionId })
await ops.commit()
}
/** @public */
export async function restoreFileVersion (
client: TxOperations,
file: Ref<File>,
version: Ref<FileVersion>
): Promise<void> {
const currentFile = await client.findOne(drive.class.File, { _id: file })
if (currentFile === undefined) {
throw new Error('file not found')
}
if (currentFile.file !== version) {
await client.update(currentFile, { file: version })
}
}

View File

@ -25,33 +25,26 @@ import {
FindOptions, FindOptions,
FindResult FindResult
} from '@hcengineering/core' } from '@hcengineering/core'
import drive, { type File, type Folder } from '@hcengineering/drive' import drive, { type FileVersion, type Folder } from '@hcengineering/drive'
import type { TriggerControl } from '@hcengineering/server-core' import type { TriggerControl } from '@hcengineering/server-core'
/** /** @public */
* @public export async function OnFileVersionDelete (
*/
export async function OnFileDelete (
tx: Tx, tx: Tx,
{ removedMap, ctx, storageAdapter, workspace }: TriggerControl { removedMap, ctx, storageAdapter, workspace }: TriggerControl
): Promise<Tx[]> { ): Promise<Tx[]> {
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<File> const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<FileVersion>
// Obtain document being deleted. // Obtain document being deleted.
const attach = removedMap.get(rmTx.objectId) as File const version = removedMap.get(rmTx.objectId) as FileVersion
if (version !== undefined) {
if (attach === undefined) { await storageAdapter.remove(ctx, workspace, [version.file])
return []
} }
await storageAdapter.remove(ctx, workspace, [attach.file])
return [] return []
} }
/** /** @public */
* @public
*/
export async function FindFolderResources ( export async function FindFolderResources (
doc: Doc, doc: Doc,
hiearachy: Hierarchy, hiearachy: Hierarchy,
@ -71,7 +64,7 @@ export async function FindFolderResources (
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({ export default async () => ({
trigger: { trigger: {
OnFileDelete OnFileVersionDelete
}, },
function: { function: {
FindFolderResources FindFolderResources

View File

@ -27,7 +27,7 @@ export const serverDriveId = 'server-drive' as Plugin
*/ */
export default plugin(serverDriveId, { export default plugin(serverDriveId, {
trigger: { trigger: {
OnFileDelete: '' as Resource<TriggerFunc> OnFileVersionDelete: '' as Resource<TriggerFunc>
}, },
function: { function: {
FindFolderResources: '' as Resource<ObjectDDParticipantFunc> FindFolderResources: '' as Resource<ObjectDDParticipantFunc>