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

View File

@ -17,45 +17,62 @@ import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import core, {
type Blob,
type Class,
type CollectionSize,
type Domain,
type FindOptions,
type Ref,
type Role,
type RolesAssignment,
Account,
AccountRole,
IndexKind,
Ref,
SortingOrder
} 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 {
type Builder,
Collection,
Hidden,
Index,
Mixin,
Model,
Prop,
ReadOnly,
TypeFileSize,
TypeRecord,
TypeRef,
TypeString,
UX,
Collection
TypeTimestamp,
UX
} 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 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 { getEmbeddedLabel } from '@hcengineering/platform'
import drive from './plugin'
export { driveId } from '@hcengineering/drive'
export { driveOperation } from './migration'
export { drive as default }
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)
@UX(drive.string.Drive)
export class TDrive extends TTypedSpace implements Drive {}
@ -75,15 +92,6 @@ export class TResource extends TDoc implements Resource {
@Index(IndexKind.FullText)
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)
@Index(IndexKind.Indexed)
@ReadOnly()
@ -95,6 +103,10 @@ export class TResource extends TDoc implements Resource {
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number
@Prop(TypeRef(drive.class.FileVersion), drive.string.Version)
@ReadOnly()
file?: Ref<FileVersion>
}
@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 file: undefined
declare preview: undefined
}
@Model(drive.class.File, drive.class.Resource, DOMAIN_DRIVE)
@UX(drive.string.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)
@Index(IndexKind.Indexed)
@ReadOnly()
@ -132,6 +135,75 @@ export class TFile extends TResource implements File {
@Prop(TypeRef(drive.class.Folder), drive.string.Path)
@ReadOnly()
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 {
@ -266,21 +338,20 @@ function defineResource (builder: Builder): void {
},
'$lookup.file.size',
'comments',
'$lookup.file.modifiedOn',
'$lookup.file.lastModified',
'createdBy'
],
/* eslint-disable @typescript-eslint/consistent-type-assertions */
options: {
lookup: {
file: core.class.Blob,
preview: core.class.Blob
file: drive.class.FileVersion
},
sort: {
_class: SortingOrder.Descending
}
} as FindOptions<Resource>,
configOptions: {
hiddenKeys: ['name', 'file', 'parent', 'path', 'type'],
hiddenKeys: ['name', 'parent', 'path', 'file', 'versions'],
sortable: true
}
},
@ -325,14 +396,13 @@ function defineResource (builder: Builder): void {
'createdBy'
],
configOptions: {
hiddenKeys: ['name', 'file', 'parent', 'path'],
hiddenKeys: ['name', 'parent', 'path', 'file', 'versions'],
sortable: true
},
/* eslint-disable @typescript-eslint/consistent-type-assertions */
options: {
lookup: {
file: core.class.Blob,
preview: core.class.Blob
file: drive.class.FileVersion
},
sort: {
_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 {
builder.createModel(TFile)
@ -506,6 +615,24 @@ function defineFile (builder: Builder): void {
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, {
...actionTemplates.move,
action: view.actionImpl.ShowPopup,
@ -580,9 +707,11 @@ export function createModel (builder: Builder): void {
drive.viewlet.Grid
)
defineTypes(builder)
defineDrive(builder)
defineResource(builder)
defineFolder(builder)
defineFile(builder)
defineFileVersion(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,
FilePresenter: '' as AnyComponent,
FileSizePresenter: '' as AnyComponent,
FileVersionPresenter: '' as AnyComponent,
FileVersionVersionPresenter: '' as AnyComponent,
MoveResource: '' as AnyComponent,
ResourcePresenter: '' as AnyComponent
},
@ -68,7 +70,8 @@ export default mergeIds(driveId, drive, {
EditDrive: '' as Ref<Action>,
DownloadFile: '' as Ref<Action>,
RenameFile: '' as Ref<Action>,
RenameFolder: '' as Ref<Action>
RenameFolder: '' as Ref<Action>,
RestoreFileVersion: '' as Ref<Action>
},
actionImpl: {
CreateChildFolder: '' as ViewAction,
@ -76,17 +79,21 @@ export default mergeIds(driveId, drive, {
EditDrive: '' as ViewAction,
DownloadFile: '' as ViewAction,
RenameFile: '' as ViewAction,
RenameFolder: '' as ViewAction
RenameFolder: '' as ViewAction,
RestoreFileVersion: '' as ViewAction
},
string: {
Grid: '' as IntlString,
Name: '' as IntlString,
Description: '' as IntlString,
Metadata: '' as IntlString,
ContentType: '' as IntlString,
Size: '' as IntlString,
LastModified: '' as IntlString,
Parent: '' as IntlString,
Path: '' as IntlString,
Drives: '' as IntlString,
Download: '' as IntlString,
Preview: '' as IntlString
Version: '' as IntlString,
Restore: '' as IntlString
}
})

View File

@ -24,10 +24,10 @@ export { serverDriveId } from '@hcengineering/server-drive'
export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverDrive.trigger.OnFileDelete,
trigger: serverDrive.trigger.OnFileVersionDelete,
txMatch: {
_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="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 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>

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -33,5 +33,5 @@
<div class="antiHSpacer x2" />
<DocsNavigator elements={parents} />
<div class="title">
<FilePresenter value={object} shouldShowAvatar={false} disabled noUnderline />
<FilePresenter value={object} shouldShowAvatar={false} shouldShowVersion disabled noUnderline />
</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.
-->
<script lang="ts">
import core, { WithLookup, type Ref } from '@hcengineering/core'
import { type File } from '@hcengineering/drive'
import { WithLookup, type Ref } from '@hcengineering/core'
import { type File, type FileVersion } from '@hcengineering/drive'
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 view from '@hcengineering/view'
import { showMenu } from '@hcengineering/view-resources'
@ -24,8 +24,11 @@
import EditFile from './EditFile.svelte'
import FileAside from './FileAside.svelte'
import FileHeader from './FileHeader.svelte'
import IconDownload from './icons/FileDownload.svelte'
import IconUpload from './icons/FileUpload.svelte'
import drive from '../plugin'
import { replaceOneFile } from '../utils'
export let _id: Ref<File>
export let readonly: boolean = false
@ -37,7 +40,9 @@
}
let object: WithLookup<File> | undefined = undefined
let version: FileVersion | undefined = undefined
let download: HTMLAnchorElement
let upload: HTMLInputElement
const query = createQuery()
$: query.query(
@ -45,16 +50,37 @@
{ _id },
(res) => {
;[object] = res
version = object?.$lookup?.file
},
{
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>
{#if object}
{#if object && version}
<Panel
{object}
{embedded}
@ -66,24 +92,39 @@
on:close
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">
<FileHeader {object} />
</svelte:fragment>
<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}>
<Button
icon={IconDownload}
iconProps={{ size: 'medium' }}
kind={'icon'}
showTooltip={{ label: presentation.string.Download }}
on:click={() => {
download.click()
}}
showTooltip={{ label: drive.string.Download }}
on:click={handleDownloadFile}
/>
</a>
{/await}
<Button
icon={IconUpload}
iconProps={{ size: 'medium' }}
kind={'icon'}
showTooltip={{ label: drive.string.Upload }}
on:click={handleUploadFile}
/>
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}

View File

@ -22,7 +22,7 @@
import { ObjectPresenterType } from '@hcengineering/view'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import { getFileTypeIcon } from '../utils'
import { formatFileVersion, getFileTypeIcon } from '../utils'
export let value: WithLookup<File>
export let inline: boolean = false
@ -32,7 +32,9 @@
export let shouldShowAvatar = true
export let type: ObjectPresenterType = 'link'
$: icon = getFileTypeIcon(value.$lookup?.file?.contentType ?? '')
export let shouldShowVersion = false
$: icon = getFileTypeIcon(value.$lookup?.file?.type ?? '')
</script>
{#if value}
@ -46,9 +48,13 @@
<Icon {icon} size={'small'} />
</div>
{/if}
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{value.name}
</span>
<div class="label nowrap flex flex-gap-2" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
<span>{value.name}</span>
{#if shouldShowVersion}
<span></span>
<span>{formatFileVersion(value.version)}</span>
{/if}
</div>
</div>
</DocNavLink>
{: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 dispatch = createEventDispatcher()
$: version = object.$lookup?.file
let hovered = false
</script>
@ -39,6 +41,7 @@
class="card-container"
class:selected
class:hovered
draggable="false"
on:mouseover={() => dispatch('obj-focus', object)}
on:mouseenter={() => dispatch('obj-focus', object)}
on:focus={() => {}}
@ -88,11 +91,11 @@
/>
<span></span>
<span class="flex-no-shrink">
<TimestampPresenter value={object.$lookup?.file?.modifiedOn ?? object.createdOn ?? object.modifiedOn} />
<TimestampPresenter value={version?.lastModified ?? object.createdOn ?? object.modifiedOn} />
</span>
</div>
<div class="flex-no-shrink font-regular-12">
<FileSizePresenter value={object.$lookup?.file?.size} />
<FileSizePresenter value={version?.size} />
</div>
</div>
</div>

View File

@ -35,17 +35,18 @@
let isImage = false
let isError = false
$: previewBlob = object.$lookup?.preview ?? object.$lookup?.file
$: previewRef = object.$lookup?.preview !== undefined ? object.preview : object.file
$: isImage = previewBlob?.contentType?.startsWith('image/') ?? false
$: version = object.$lookup?.file
$: previewRef = version?.file
$: isImage = version?.type?.startsWith('image/') ?? false
$: isFolder = hierarchy.isDerived(object._class, drive.class.Folder)
</script>
{#if isFolder}
<Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--theme-trans-color)'} />
{:else if previewBlob != null && previewRef != null && isImage && !isError}
{#await getBlobRef(previewBlob, previewRef, object.name, sizeToWidth(size)) then blobSrc}
{:else if previewRef != null && isImage && !isError}
{#await getBlobRef(undefined, previewRef, object.name, sizeToWidth(size)) then blobSrc}
<img
draggable="false"
class="img-fit"
src={blobSrc.src}
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 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 { getBlobHref } from '@hcengineering/presentation'
import { showPopup, type Location } from '@hcengineering/ui'
@ -29,6 +29,8 @@ import EditFolder from './components/EditFolder.svelte'
import FilePanel from './components/FilePanel.svelte'
import FilePresenter from './components/FilePresenter.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 FolderPresenter from './components/FolderPresenter.svelte'
import GridView from './components/GridView.svelte'
@ -36,7 +38,7 @@ import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte'
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> {
await createFolder(doc._id, drive.ids.Root)
@ -53,13 +55,16 @@ async function EditDrive (drive: Drive): Promise<void> {
async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> {
const files = Array.isArray(doc) ? doc : [doc]
for (const file of files) {
const href = await getBlobHref(file.$lookup?.file, file.file, file.name)
const link = document.createElement('a')
link.style.display = 'none'
link.target = '_blank'
link.href = href
link.download = file.name
link.click()
const version = file.$lookup?.file
if (version != null) {
const href = await getBlobHref(undefined, version.file, version.name)
const link = document.createElement('a')
link.style.display = 'none'
link.target = '_blank'
link.href = href
link.download = file.name
link.click()
}
}
}
@ -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> {
return doc !== undefined && !Array.isArray(doc)
}
@ -107,6 +118,8 @@ export default async (): Promise<Resources> => ({
FilePanel,
FilePresenter,
FileSizePresenter,
FileVersionPresenter,
FileVersionVersionPresenter,
FolderPanel,
FolderPresenter,
GridView,
@ -119,7 +132,8 @@ export default async (): Promise<Resources> => ({
EditDrive,
DownloadFile,
RenameFile,
RenameFolder
RenameFolder,
RestoreFileVersion
},
function: {
DriveLinkProvider,

View File

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

View File

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

View File

@ -13,8 +13,9 @@
// limitations under the License.
//
import { toIdMap, type Class, type Doc, type Ref } from '@hcengineering/core'
import drive, { type Drive, type Folder, type Resource } from '@hcengineering/drive'
import { type Class, type Doc, type Ref, toIdMap } from '@hcengineering/core'
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 { getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
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> {
showPopup(CreateFolder, { space, parent }, 'top', async (id) => {
if (open && id !== undefined && id !== null) {
@ -58,32 +63,42 @@ export async function editDrive (drive: Drive): Promise<void> {
showPopup(CreateDrive, { drive })
}
export async function createFiles (list: FileList, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
const client = getClient()
const folder = await client.findOne(drive.class.Folder, { space, _id: parent })
export async function uploadFiles (list: FileList, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
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()
try {
const uuid = await uploadFile(file)
const metadata = await getFileMetadata(file, uuid)
await client.createDoc(drive.class.File, space, {
name: file.name,
file: uuid,
metadata,
parent: parent?._id ?? drive.ids.Root,
path: parent !== undefined ? [parent._id, ...parent.path] : []
})
const { name, size, type, lastModified } = file
const data = { file: uuid, name, size, type, lastModified, metadata }
await createFile(client, space, parent, data)
} catch (e) {
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) {
void setPlatformStatus(unknownError(e))
}
@ -130,6 +145,15 @@ export async function moveResources (resources: Resource[], space: Ref<Drive>, p
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> = {
'application/pdf': FileTypePdf,
audio: FileTypeAudio,

View File

@ -16,6 +16,7 @@
import { driveId, drivePlugin } from './plugin'
export * from './types'
export * from './utils'
export { driveId }
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 { plugin } from '@hcengineering/platform'
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'
@ -30,9 +30,10 @@ export const drivePlugin = plugin(driveId, {
class: {
Drive: '' as Ref<Class<Drive>>,
File: '' as Ref<Class<File>>,
FileVersion: '' as Ref<Class<FileVersion>>,
Folder: '' as Ref<Class<Folder>>,
Resource: '' as Ref<Class<Resource>>,
TypeFileSize: '' as Ref<Class<Type<FileSize>>>
TypeFileVersion: '' as Ref<Class<Type<number>>>
},
mixin: {
DefaultDriveTypeData: '' as Ref<Mixin<Drive>>
@ -44,7 +45,8 @@ export const drivePlugin = plugin(driveId, {
Folder: '' as Asset,
FolderOpen: '' as Asset,
FolderClosed: '' as Asset,
Download: '' as Asset
Download: '' as Asset,
Restore: '' as Asset
},
app: {
Drive: '' as Ref<Doc>
@ -58,6 +60,7 @@ export const drivePlugin = plugin(driveId, {
string: {
Drive: '' as IntlString,
File: '' as IntlString,
FileVersion: '' as IntlString,
Folder: '' as IntlString,
Resource: '' as IntlString
},

View File

@ -13,10 +13,14 @@
// 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 */
export type FileSize = number
export function TypeFileVersion (): Type<number> {
return { _class: drive.class.TypeFileVersion, label: drive.string.FileVersion }
}
/** @public */
export interface Drive extends TypedSpace {}
@ -24,13 +28,13 @@ export interface Drive extends TypedSpace {}
/** @public */
export interface Resource extends Doc<Drive> {
name: string
file?: Ref<Blob>
preview?: Ref<Blob>
parent: Ref<Resource>
path: Ref<Resource>[]
comments?: number
// ugly but needed here to get version lookup work for Resource
file?: Ref<FileVersion>
}
/** @public */
@ -43,9 +47,21 @@ export interface Folder extends Resource {
/** @public */
export interface File extends Resource {
file: Ref<Blob>
metadata?: Record<string, any>
parent: 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,
FindResult
} 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'
/**
* @public
*/
export async function OnFileDelete (
/** @public */
export async function OnFileVersionDelete (
tx: Tx,
{ removedMap, ctx, storageAdapter, workspace }: TriggerControl
): Promise<Tx[]> {
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<File>
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<FileVersion>
// Obtain document being deleted.
const attach = removedMap.get(rmTx.objectId) as File
if (attach === undefined) {
return []
const version = removedMap.get(rmTx.objectId) as FileVersion
if (version !== undefined) {
await storageAdapter.remove(ctx, workspace, [version.file])
}
await storageAdapter.remove(ctx, workspace, [attach.file])
return []
}
/**
* @public
*/
/** @public */
export async function FindFolderResources (
doc: Doc,
hiearachy: Hierarchy,
@ -71,7 +64,7 @@ export async function FindFolderResources (
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
OnFileDelete
OnFileVersionDelete
},
function: {
FindFolderResources

View File

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