mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
UBERF-7356 Drive file versions (#6049)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
f9962fe086
commit
d2706ffed8
@ -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]
|
||||
|
@ -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)
|
||||
}
|
||||
|
102
models/drive/src/migration.ts
Normal file
102
models/drive/src/migration.ts
Normal 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, [])
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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 |
@ -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": "/"
|
||||
}
|
||||
|
@ -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": "/"
|
||||
}
|
||||
|
@ -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": "/"
|
||||
}
|
||||
|
@ -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": "/"
|
||||
}
|
||||
|
@ -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": "/"
|
||||
}
|
||||
|
@ -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": "/"
|
||||
}
|
||||
|
@ -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`
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
46
plugins/drive-resources/src/components/FileInput.svelte
Normal file
46
plugins/drive-resources/src/components/FileInput.svelte
Normal 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}
|
||||
/>
|
@ -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' }}
|
||||
|
@ -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'}
|
||||
|
@ -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}
|
@ -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}
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -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>
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -16,6 +16,7 @@
|
||||
import { driveId, drivePlugin } from './plugin'
|
||||
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
export { driveId }
|
||||
|
||||
export default drivePlugin
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
98
plugins/drive/src/utils.ts
Normal file
98
plugins/drive/src/utils.ts
Normal 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 })
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user