Skip to content

Commit

Permalink
Duplicate and restore project (#9816)
Browse files Browse the repository at this point in the history
- Close enso-org/cloud-v2#943
- Add buttons in "versions" tab on the right hand side panel to restore and duplicate projects.

# Important Notes
- There is an UI issue when restoring a project - the placeholder UI is removed one render tick later than the "list versions" query is refetched, so an extra version appears for one frame.
  • Loading branch information
somebody1234 authored May 30, 2024
1 parent 65737b3 commit c94507d
Show file tree
Hide file tree
Showing 21 changed files with 382 additions and 90 deletions.
5 changes: 5 additions & 0 deletions app/ide-desktop/lib/assets/compare.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 19 additions & 15 deletions app/ide-desktop/lib/assets/reload_in_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions app/ide-desktop/lib/assets/restore.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const OVERLAY_STYLES = twv.tv({
})

const MODAL_STYLES = twv.tv({
base: 'fixed inset-0 flex items-center justify-center text-center p-4',
base: 'fixed inset-0 flex items-center justify-center text-center text-xs text-primary p-4',
variants: {
isEntering: { true: 'animate-in slide-in-from-top-1 ease-out duration-200' },
isExiting: { true: 'animate-out slide-out-to-top-1 ease-in duration-200' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,17 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
if (asset.id === event.placeholderId) {
rowState.setVisibility(Visibility.faded)
try {
const createdProject = await backend.createProject({
parentDirectoryId: asset.parentId,
projectName: asset.title,
...(event.templateId == null ? {} : { projectTemplateName: event.templateId }),
...(event.datalinkId == null ? {} : { datalinkId: event.datalinkId }),
})
const createdProject =
event.originalId == null || event.versionId == null
? await backend.createProject({
parentDirectoryId: asset.parentId,
projectName: asset.title,
...(event.templateId == null
? {}
: { projectTemplateName: event.templateId }),
...(event.datalinkId == null ? {} : { datalinkId: event.datalinkId }),
})
: await backend.duplicateProject(event.originalId, event.versionId, asset.title)
rowState.setVisibility(Visibility.visible)
setAsset(
object.merge(asset, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ enum AssetListEventType {
newDatalink = 'new-datalink',
newSecret = 'new-secret',
insertAssets = 'insert-assets',
duplicateProject = 'duplicate-project',
closeFolder = 'close-folder',
copy = 'copy',
move = 'move',
Expand Down
2 changes: 2 additions & 0 deletions app/ide-desktop/lib/dashboard/src/events/assetEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export interface AssetNewProjectEvent extends AssetBaseEvent<AssetEventType.newP
readonly placeholderId: backend.ProjectId
readonly templateId: string | null
readonly datalinkId: backend.DatalinkId | null
readonly originalId: backend.ProjectId | null
readonly versionId: backend.S3ObjectVersionId | null
readonly onSpinnerStateChange: ((state: spinner.SpinnerState) => void) | null
}

Expand Down
10 changes: 10 additions & 0 deletions app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface AssetListEvents {
readonly newSecret: AssetListNewSecretEvent
readonly newDatalink: AssetListNewDatalinkEvent
readonly insertAssets: AssetListInsertAssetsEvent
readonly duplicateProject: AssetListDuplicateProjectEvent
readonly closeFolder: AssetListCloseFolderEvent
readonly copy: AssetListCopyEvent
readonly move: AssetListMoveEvent
Expand Down Expand Up @@ -97,6 +98,15 @@ interface AssetListInsertAssetsEvent extends AssetListBaseEvent<AssetListEventTy
readonly assets: backend.AnyAsset[]
}

/** A signal to duplicate a project. */
interface AssetListDuplicateProjectEvent
extends AssetListBaseEvent<AssetListEventType.duplicateProject> {
readonly parentKey: backend.DirectoryId
readonly parentId: backend.DirectoryId
readonly original: backend.ProjectAsset
readonly versionId: backend.S3ObjectVersionId
}

/** A signal to close (collapse) a folder. */
interface AssetListCloseFolderEvent extends AssetListBaseEvent<AssetListEventType.closeFolder> {
readonly id: backend.DirectoryId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @file Diff view comparing `Main.enso` of two versions for a specific project. */
import * as react from '@monaco-editor/react'
import * as monacoReact from '@monaco-editor/react'

import * as textProvider from '#/providers/TextProvider'

Expand Down Expand Up @@ -50,7 +50,7 @@ export function AssetDiffView(props: AssetDiffViewProps) {
return loader
} else {
return (
<react.DiffEditor
<monacoReact.DiffEditor
beforeMount={monaco => {
monaco.editor.defineTheme('myTheme', {
base: 'vs',
Expand Down
17 changes: 7 additions & 10 deletions app/ide-desktop/lib/dashboard/src/layouts/AssetPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as localStorageProvider from '#/providers/LocalStorageProvider'
import * as textProvider from '#/providers/TextProvider'

import type * as assetEvent from '#/events/assetEvent'
import type * as assetListEvent from '#/events/assetListEvent'

import AssetProperties from '#/layouts/AssetProperties'
import AssetVersions from '#/layouts/AssetVersions/AssetVersions'
Expand Down Expand Up @@ -62,19 +63,13 @@ export interface AssetPanelProps extends AssetPanelRequiredProps {
readonly category: Category
readonly labels: backend.Label[]
readonly dispatchAssetEvent: (event: assetEvent.AssetEvent) => void
readonly dispatchAssetListEvent: (event: assetListEvent.AssetListEvent) => void
}

/** A panel containing the description and settings for an asset. */
export default function AssetPanel(props: AssetPanelProps) {
const {
item,
setItem,
setQuery,
category,
labels,
dispatchAssetEvent,
isReadonly = false,
} = props
const { item, isReadonly = false, setItem, setQuery, category, labels } = props
const { dispatchAssetEvent, dispatchAssetListEvent } = props

const { getText } = textProvider.useText()
const { localStorage } = localStorageProvider.useLocalStorage()
Expand Down Expand Up @@ -153,7 +148,9 @@ export default function AssetPanel(props: AssetPanelProps) {
dispatchAssetEvent={dispatchAssetEvent}
/>
)}
{tab === AssetPanelTab.versions && <AssetVersions item={item} />}
{tab === AssetPanelTab.versions && (
<AssetVersions item={item} dispatchAssetListEvent={dispatchAssetListEvent} />
)}
</>
)}
</div>
Expand Down
117 changes: 102 additions & 15 deletions app/ide-desktop/lib/dashboard/src/layouts/AssetVersion.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,66 @@
/** @file Displays information describing a specific version of an asset. */
import Duplicate from 'enso-assets/duplicate.svg'
import * as React from 'react'

import CompareIcon from 'enso-assets/compare.svg'
import DuplicateIcon from 'enso-assets/duplicate.svg'
import RestoreIcon from 'enso-assets/restore.svg'

import * as textProvider from '#/providers/TextProvider'

import type * as assetListEvent from '#/events/assetListEvent'
import AssetListEventType from '#/events/AssetListEventType'

import * as assetDiffView from '#/layouts/AssetDiffView'

import * as ariaComponents from '#/components/AriaComponents'
import ButtonRow from '#/components/styled/ButtonRow'

import type Backend from '#/services/Backend'
import * as backendService from '#/services/Backend'

import type AssetTreeNode from '#/utilities/AssetTreeNode'
import * as dateTime from '#/utilities/dateTime'

import * as assetDiffView from './AssetDiffView'

// ====================
// === AssetVersion ===
// ====================

/** Props for a {@link AssetVersion}. */
export interface AssetVersionProps {
readonly item: backendService.AnyAsset
readonly placeholder?: boolean
readonly item: AssetTreeNode
readonly number: number
readonly version: backendService.S3ObjectVersion
readonly latestVersion: backendService.S3ObjectVersion
readonly backend: Backend
readonly dispatchAssetListEvent: (event: assetListEvent.AssetListEvent) => void
readonly doRestore: () => Promise<void> | void
}

/** Displays information describing a specific version of an asset. */
export default function AssetVersion(props: AssetVersionProps) {
const { number, version, item, backend, latestVersion } = props
const { placeholder = false, number, version, item, backend, latestVersion } = props
const { dispatchAssetListEvent, doRestore } = props
const { getText } = textProvider.useText()
const asset = item.item
const isProject = asset.type === backendService.AssetType.project

const isProject = item.type === backendService.AssetType.project
const doDuplicate = () => {
if (isProject) {
dispatchAssetListEvent({
type: AssetListEventType.duplicateProject,
parentKey: item.directoryKey,
parentId: asset.parentId,
original: asset,
versionId: version.versionId,
})
}
}

return (
<div className="flex w-full flex-shrink-0 basis-0 select-none flex-row gap-4 rounded-2xl p-2">
<div
className={`flex w-full flex-shrink-0 basis-0 select-none flex-row gap-4 rounded-2xl p-2 ${placeholder ? 'opacity-50' : ''}`}
>
<div className="flex flex-1 flex-col">
<div>
{getText('versionX', number)} {version.isLatest && getText('latestIndicator')}
Expand All @@ -51,24 +78,84 @@ export default function AssetVersion(props: AssetVersionProps) {
<ariaComponents.Button
variant="icon"
aria-label={getText('compareWithLatest')}
icon={Duplicate}
isDisabled={version.isLatest}
icon={CompareIcon}
isDisabled={version.isLatest || placeholder}
/>
<ariaComponents.Tooltip>{getText('compareWithLatest')}</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
<ariaComponents.Dialog
type="fullscreen"
title={getText('compareVersionXWithLatest', number)}
>
<assetDiffView.AssetDiffView
latestVersionId={latestVersion.versionId}
versionId={version.versionId}
project={item}
backend={backend}
/>
{opts => (
<div className="flex h-full flex-col gap-3">
<ButtonRow>
<ariaComponents.TooltipTrigger>
<ariaComponents.Button
variant="icon"
aria-label={getText('restoreThisVersion')}
icon={RestoreIcon}
isDisabled={version.isLatest || placeholder}
onPress={async () => {
await doRestore()
opts.close()
}}
/>
<ariaComponents.Tooltip>
{getText('restoreThisVersion')}
</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
<ariaComponents.TooltipTrigger>
<ariaComponents.Button
variant="icon"
aria-label={getText('duplicateThisVersion')}
icon={DuplicateIcon}
isDisabled={placeholder}
onPress={() => {
doDuplicate()
opts.close()
}}
/>
<ariaComponents.Tooltip>
{getText('duplicateThisVersion')}
</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
</ButtonRow>
<assetDiffView.AssetDiffView
latestVersionId={latestVersion.versionId}
versionId={version.versionId}
project={asset}
backend={backend}
/>
</div>
)}
</ariaComponents.Dialog>
</ariaComponents.DialogTrigger>
)}
{isProject && (
<ariaComponents.TooltipTrigger>
<ariaComponents.Button
variant="icon"
aria-label={getText('restoreThisVersion')}
icon={RestoreIcon}
isDisabled={version.isLatest || placeholder}
onPress={doRestore}
/>
<ariaComponents.Tooltip>{getText('restoreThisVersion')}</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)}
{isProject && (
<ariaComponents.TooltipTrigger>
<ariaComponents.Button
variant="icon"
aria-label={getText('duplicateThisVersion')}
icon={DuplicateIcon}
isDisabled={placeholder}
onPress={doDuplicate}
/>
<ariaComponents.Tooltip>{getText('duplicateThisVersion')}</ariaComponents.Tooltip>
</ariaComponents.TooltipTrigger>
)}
</div>
</div>
)
Expand Down
Loading

0 comments on commit c94507d

Please sign in to comment.