mirror of
https://github.com/enso-org/enso.git
synced 2024-10-26 13:14:43 +03:00
Re-organize lib/dashboard/
(#8587)
- Significantly flattens directory structure of `lib/dashboard/` # Important Notes - Basic testing done on: - dashboard's `npm run dev` which (since quite recently) uses Vite. - specifically: `npm run dev` in `app/ide-desktop/lib/dashboard`, OR `npm run dashboard:dev` in `app/ide-desktop` - dashboard's bundle script (`npm run build`) which uses ESBuild. - GUI2's own entry point (GUI2's `npm run dev`). - `./run ide build` - `./run ide watch` - `./run ide2 build` - `./run gui watch`
This commit is contained in:
parent
f31ecc7c87
commit
8597de1d43
@ -9,7 +9,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "npm --workspace enso-authentication run compile && run-p typecheck build-only",
|
||||
"build": "npm --workspace enso-dashboard run compile && run-p typecheck build-only",
|
||||
"build:cloud": "cross-env CLOUD_BUILD=true npm run build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run && playwright test --reporter=html",
|
||||
@ -52,7 +52,7 @@
|
||||
"ag-grid-enterprise": "^30.2.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"culori": "^3.2.0",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-dashboard": "^0.1.0",
|
||||
"events": "^3.3.0",
|
||||
"fast-diff": "^1.3.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
@ -129,6 +129,7 @@
|
||||
"vite-plugin-inspect": "^0.7.38",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-react-wrapper": "^0.3.1",
|
||||
"vue-tsc": "^1.8.8"
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ import * as set from 'lib0/set'
|
||||
import { toast } from 'react-toastify'
|
||||
import type { ExprId, NodeMetadata } from 'shared/yjsModel'
|
||||
import { computed, onMounted, onScopeDispose, onUnmounted, ref, watch } from 'vue'
|
||||
import { ProjectManagerEvents } from '../../../ide-desktop/lib/dashboard/src/authentication/src/dashboard/projectManager'
|
||||
import { ProjectManagerEvents } from '../../../ide-desktop/lib/dashboard/src/utilities/projectManager'
|
||||
import { type Usage } from './ComponentBrowser/input'
|
||||
|
||||
const EXECUTION_MODES = ['design', 'live']
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { baseConfig, configValue, mergeConfig } from '@/util/config'
|
||||
import { isDevMode } from '@/util/detect'
|
||||
import { urlParams } from '@/util/urlParams'
|
||||
import { run as runDashboard } from 'enso-authentication'
|
||||
import { isOnLinux } from 'enso-common/src/detect'
|
||||
import * as dashboard from 'enso-dashboard'
|
||||
import 'enso-dashboard/src/tailwind.css'
|
||||
|
||||
const INITIAL_URL_KEY = `Enso-initial-url`
|
||||
@ -106,7 +106,7 @@ function main() {
|
||||
const projectManagerUrl = config.engine.projectManagerUrl || PROJECT_MANAGER_URL
|
||||
const initialProjectName = config.startup.project || null
|
||||
|
||||
runDashboard({
|
||||
dashboard.run({
|
||||
appRunner,
|
||||
logger: console,
|
||||
supportsLocalBackend: !IS_CLOUD_BUILD,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ide-desktop/lib/dashboard/src/authentication/tsconfig.json"
|
||||
"path": "../ide-desktop/lib/dashboard/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
|
8
app/ide-desktop/.vscode/settings.json
vendored
Normal file
8
app/ide-desktop/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/** @file Debug-only functions, injected or stripped by the build tool as appropriate. */
|
||||
|
||||
/** The logger used by {@link assert}. */
|
||||
interface Logger {
|
||||
error: (message: string) => void
|
||||
}
|
||||
|
||||
/** Logs an error . */
|
||||
export function assert(invariant: boolean, message: string, logger: Logger = console) {
|
||||
if (!invariant) {
|
||||
logger.error('assertion failed: ' + message)
|
||||
}
|
||||
}
|
||||
|
||||
// This is required to make the definition `Object.prototype.$d$` not error.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
declare global {
|
||||
// Documentation is already inherited.
|
||||
/** */
|
||||
interface Object {
|
||||
/** Log self and return self. */
|
||||
$d$: <T>(this: T, message?: string) => T
|
||||
}
|
||||
}
|
||||
|
||||
if (!('$d$' in Object.prototype)) {
|
||||
Object.defineProperty(Object.prototype, '$d$', {
|
||||
/** Log self and return self. */
|
||||
value: function <T>(this: T, message?: string) {
|
||||
if (message != null) {
|
||||
console.log(message, this)
|
||||
} else {
|
||||
console.log(this)
|
||||
}
|
||||
return this
|
||||
},
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
@ -30,11 +30,11 @@ const NAME = 'enso'
|
||||
* `yargs` is a modules we explicitly want the default imports of.
|
||||
* `node:process` is here because `process.on` does not exist on the namespace import. */
|
||||
const DEFAULT_IMPORT_ONLY_MODULES =
|
||||
'node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|enso-assets.*|@modyfi\\u002Fvite-plugin-yaml|validator.+'
|
||||
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss`
|
||||
'@vitejs\\u002Fplugin-react|node:process|chalk|string-length|yargs|yargs\\u002Fyargs|sharp|to-ico|connect|morgan|serve-static|create-servers|electron-is-dev|fast-glob|esbuild-plugin-.+|opener|tailwindcss.*|enso-assets.*|@modyfi\\u002Fvite-plugin-yaml|validator.+'
|
||||
const OUR_MODULES = 'enso-.*'
|
||||
const RELATIVE_MODULES =
|
||||
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations'
|
||||
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|detect|file-associations|index|ipc|log|naming|paths|preload|project-management|security|url-associations|#\\u002F.*'
|
||||
const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|postcss|${RELATIVE_MODULES}`
|
||||
const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
|
||||
const JSX = ':matches(JSXElement, JSXFragment)'
|
||||
const NOT_PASCAL_CASE = '/^(?!do[A-Z])(?!_?([A-Z][a-z0-9]*)+$)/'
|
||||
@ -89,10 +89,6 @@ const RESTRICTED_SYNTAXES = [
|
||||
selector: `:matches(ImportDefaultSpecifier[local.name=/^${NAME}/i], ImportNamespaceSpecifier > Identifier[name=/^${NAME}/i])`,
|
||||
message: `Don't prefix modules with \`${NAME}\``,
|
||||
},
|
||||
{
|
||||
selector: 'ExportAllDeclaration',
|
||||
message: 'No re-exports',
|
||||
},
|
||||
{
|
||||
selector: 'TSTypeLiteral',
|
||||
message: 'No object types - use interfaces instead',
|
||||
@ -299,7 +295,6 @@ export default [
|
||||
],
|
||||
},
|
||||
],
|
||||
'sort-imports': ['error', { allowSeparatedGroups: true }],
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
@ -516,10 +511,6 @@ export default [
|
||||
property: 'useDebugCallback',
|
||||
message: 'Avoid leaving debugging statements when committing code',
|
||||
},
|
||||
{
|
||||
property: '$d$',
|
||||
message: 'Avoid leaving debugging statements when committing code',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -559,10 +550,6 @@ export default [
|
||||
property: 'useDebugCallback',
|
||||
message: 'Avoid leaving debugging statements when committing code',
|
||||
},
|
||||
{
|
||||
property: '$d$',
|
||||
message: 'Avoid leaving debugging statements when committing code',
|
||||
},
|
||||
{
|
||||
object: 'page',
|
||||
property: 'type',
|
||||
|
@ -50,7 +50,7 @@
|
||||
"dmg-license": "^1.0.11"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "npm run --workspace=enso-gui2 compile-server && tsc --noEmit",
|
||||
"typecheck": "npm run --workspace=enso-gui2 compile-server && tsc --build",
|
||||
"start": "tsx start.ts",
|
||||
"build": "tsx bundle.ts",
|
||||
"dist": "tsx dist.ts",
|
||||
|
@ -7,14 +7,12 @@
|
||||
"include": [
|
||||
".",
|
||||
"../content",
|
||||
"../dashboard",
|
||||
"../types",
|
||||
"../../utils.ts",
|
||||
"../../../../build.json"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../gui2/tsconfig.server.json"
|
||||
}
|
||||
{ "path": "../../../gui2/tsconfig.server.json" },
|
||||
{ "path": "../dashboard" }
|
||||
]
|
||||
}
|
||||
|
@ -10,7 +10,6 @@
|
||||
import * as childProcess from 'node:child_process'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import * as url from 'node:url'
|
||||
import process from 'node:process'
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
@ -35,8 +34,6 @@ interface Watches {
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The path of this file. */
|
||||
const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
|
||||
const IDE_DIR_PATH = paths.getIdeDirectory()
|
||||
const PROJECT_MANAGER_BUNDLE_PATH = paths.getProjectManagerBundlePath()
|
||||
|
||||
@ -110,9 +107,6 @@ const ALL_BUNDLES_READY = new Promise<Watches>((resolve, reject) => {
|
||||
},
|
||||
})
|
||||
contentOpts.pure.splice(contentOpts.pure.indexOf('assert'), 1)
|
||||
;(contentOpts.inject = contentOpts.inject ?? []).push(
|
||||
path.resolve(THIS_PATH, '..', '..', 'debugGlobals.ts')
|
||||
)
|
||||
contentOpts.outdir = path.resolve(IDE_DIR_PATH, 'assets')
|
||||
contentOpts.define.REDIRECT_OVERRIDE = JSON.stringify('http://localhost:8080')
|
||||
const contentBuilder = await esbuild.context(contentOpts)
|
||||
|
@ -107,7 +107,7 @@ export function bundlerOptions(args: Arguments) {
|
||||
'.ttf': 'copy',
|
||||
},
|
||||
entryPoints: [
|
||||
pathModule.resolve(THIS_PATH, 'src', 'index.ts'),
|
||||
pathModule.resolve(THIS_PATH, 'src', 'entrypoint.ts'),
|
||||
pathModule.resolve(THIS_PATH, 'src', 'index.html'),
|
||||
pathModule.resolve(THIS_PATH, 'src', 'run.js'),
|
||||
pathModule.resolve(THIS_PATH, 'src', 'style.css'),
|
||||
|
@ -15,7 +15,7 @@
|
||||
"url": "https://github.com/enso-org/ide/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck": "tsc --build",
|
||||
"build": "tsx bundle.ts",
|
||||
"watch": "tsx watch.ts",
|
||||
"start": "tsx start.ts"
|
||||
@ -37,7 +37,7 @@
|
||||
"@types/ws": "^8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-dashboard": "^0.1.0",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-copy-directories": "^1.0.0",
|
||||
"esbuild-plugin-time": "^1.0.0",
|
||||
|
@ -8,7 +8,7 @@ import * as toastify from 'react-toastify'
|
||||
import * as app from 'ensogl-runner/src/runner'
|
||||
import * as common from 'enso-common'
|
||||
import * as contentConfig from 'enso-content-config'
|
||||
import * as dashboard from 'enso-authentication'
|
||||
import * as dashboard from 'enso-dashboard'
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
import * as gtag from 'enso-common/src/gtag'
|
||||
|
@ -37,7 +37,7 @@
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<!-- Generated by the build script based on the Enso Font package. -->
|
||||
<link rel="stylesheet" href="./ensoFont.css" />
|
||||
<script type="module" src="./index.js" defer></script>
|
||||
<script type="module" src="./entrypoint.js" defer></script>
|
||||
<script type="module" src="./run.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -2,7 +2,7 @@
|
||||
* {@link RemoteLogger} provides a convenient way to manage remote logging with access token authorization. */
|
||||
|
||||
import * as app from 'ensogl-runner/src/runner'
|
||||
import * as authConfig from '../../dashboard/src/authentication/src/config'
|
||||
import * as authConfig from '../../dashboard/src/utilities/config'
|
||||
|
||||
const logger = app.log.logger
|
||||
|
||||
@ -50,14 +50,12 @@ export async function remoteLog(
|
||||
metadata: unknown
|
||||
): Promise<void> {
|
||||
try {
|
||||
const headers: HeadersInit = new Headers()
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
const response = await fetch(REMOTE_LOG_URL, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ message, metadata }),
|
||||
})
|
||||
const headers: HeadersInit = [
|
||||
['Content-Type', 'application/json'],
|
||||
['Authorization', `Bearer ${accessToken}`],
|
||||
]
|
||||
const body = JSON.stringify({ message, metadata })
|
||||
const response = await fetch(REMOTE_LOG_URL, { method: 'POST', headers, body })
|
||||
if (!response.ok) {
|
||||
const errorMessage = `Error while sending log to a remote: Status ${response.status}.`
|
||||
try {
|
||||
|
@ -1,10 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"../types",
|
||||
"../../utils.ts",
|
||||
"../../../../build.json",
|
||||
"../dashboard",
|
||||
"."
|
||||
]
|
||||
"include": ["../types", "../../utils.ts", "../../../../build.json", "."],
|
||||
"references": [{ "path": "../dashboard" }]
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ async function watch() {
|
||||
})
|
||||
)
|
||||
opts.pure.splice(opts.pure.indexOf('assert'), 1)
|
||||
;(opts.inject = opts.inject ?? []).push(path.resolve(THIS_PATH, '..', '..', 'debugGlobals.ts'))
|
||||
opts.define.REDIRECT_OVERRIDE = JSON.stringify('http://localhost:8080')
|
||||
// This is safe as this entry point is statically known.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
36
app/ide-desktop/lib/dashboard/.prettierrc.cjs
Normal file
36
app/ide-desktop/lib/dashboard/.prettierrc.cjs
Normal file
@ -0,0 +1,36 @@
|
||||
/** @file Prettier configuration. */
|
||||
// @ts-check
|
||||
/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.[j|t]s', '*.[j|t]sx', '*.m[j|t]s', '*.c[j|t]s'],
|
||||
options: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
printWidth: 100,
|
||||
tabWidth: 4,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
arrowParens: 'avoid',
|
||||
plugins: ['@ianvs/prettier-plugin-sort-imports'],
|
||||
// This plugin's options
|
||||
importOrder: [
|
||||
'^react$',
|
||||
'',
|
||||
'<THIRD_PARTY_MODULES>',
|
||||
'',
|
||||
'^enso-',
|
||||
'',
|
||||
'^#[/](?!components[/]).*$',
|
||||
'',
|
||||
'^#[/]components[/]',
|
||||
'',
|
||||
'^[.]',
|
||||
],
|
||||
importOrderParserPlugins: ['typescript', 'jsx', 'importAssertions'],
|
||||
importOrderTypeScriptVersion: '5.0.0',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
@ -32,7 +32,7 @@ async function bundle() {
|
||||
})
|
||||
opts.entryPoints.push(
|
||||
path.resolve(THIS_PATH, 'src', 'index.html'),
|
||||
path.resolve(THIS_PATH, 'src', 'index.ts')
|
||||
path.resolve(THIS_PATH, 'src', 'entrypoint.ts')
|
||||
)
|
||||
opts.metafile = ANALYZE
|
||||
opts.loader['.html'] = 'copy'
|
||||
|
@ -10,18 +10,17 @@ import * as fs from 'node:fs/promises'
|
||||
import * as path from 'node:path'
|
||||
import * as url from 'node:url'
|
||||
|
||||
import type * as esbuild from 'esbuild'
|
||||
import * as esbuildPluginNodeModules from '@esbuild-plugins/node-modules-polyfill'
|
||||
import type * as esbuild from 'esbuild'
|
||||
import esbuildPluginInlineImage from 'esbuild-plugin-inline-image'
|
||||
import esbuildPluginTime from 'esbuild-plugin-time'
|
||||
import esbuildPluginYaml from 'esbuild-plugin-yaml'
|
||||
|
||||
import postcss from 'postcss'
|
||||
import tailwindcss from 'tailwindcss'
|
||||
import tailwindcssNesting from 'tailwindcss/nesting/index.js'
|
||||
|
||||
import * as tailwindConfig from './tailwind.config'
|
||||
import * as utils from '../../utils'
|
||||
import * as tailwindConfig from './tailwind.config'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -67,7 +66,9 @@ export function esbuildPluginGenerateTailwind(): esbuild.Plugin {
|
||||
)
|
||||
build.onLoad({ filter: /tailwind\.css$/ }, async loadArgs => {
|
||||
const content = await fs.readFile(loadArgs.path, 'utf8')
|
||||
const result = await cssProcessor.process(content, { from: loadArgs.path })
|
||||
const result = await cssProcessor.process(content, {
|
||||
from: loadArgs.path,
|
||||
})
|
||||
return {
|
||||
contents: result.content,
|
||||
loader: 'css',
|
||||
@ -117,6 +118,9 @@ export function bundlerOptions(args: Arguments) {
|
||||
esbuildPluginYaml.yamlPlugin({}),
|
||||
esbuildPluginGenerateTailwind(),
|
||||
],
|
||||
alias: {
|
||||
'#': './src',
|
||||
},
|
||||
define: {
|
||||
// We are defining constants, so it should be `CONSTANT_CASE`.
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
@ -35,7 +35,7 @@
|
||||
<title>Enso</title>
|
||||
<!-- Generated by the build script based on the Enso Font package. -->
|
||||
<link rel="stylesheet" href="./src/ensoFont.css" />
|
||||
<script type="module" src="./src/index.ts" defer></script>
|
||||
<script type="module" src="./src/entrypoint.ts" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
@ -36,9 +36,12 @@ import type * as amplify from '@aws-amplify/auth'
|
||||
import type * as cognito from 'amazon-cognito-identity-js'
|
||||
import * as results from 'ts-results'
|
||||
|
||||
import type * as config from '../../../../src/authentication/src/authentication/config'
|
||||
import type * as loggerProvider from '../../../../src/authentication/src/providers/logger'
|
||||
import * as original from '../../../../src/authentication/src/authentication/cognito'
|
||||
import * as original from '../../src/authentication/cognito'
|
||||
import type * as config from '../../src/authentication/config'
|
||||
import type * as loggerProvider from '../../src/providers/LoggerProvider'
|
||||
/* eslint-enable no-restricted-syntax */
|
||||
|
||||
import * as listen from './listen'
|
||||
|
||||
// This file exports a subset of the values from the original file.
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
@ -49,10 +52,7 @@ export {
|
||||
ForgotPasswordSubmitErrorKind,
|
||||
SignInWithPasswordErrorKind,
|
||||
SignUpErrorKind,
|
||||
} from '../../../../src/authentication/src/authentication/cognito'
|
||||
/* eslint-enable no-restricted-syntax */
|
||||
|
||||
import * as listen from './listen'
|
||||
} from '../../src/authentication/cognito'
|
||||
|
||||
// There are unused function parameters in this file.
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
@ -2,9 +2,19 @@
|
||||
"name": "enso-dashboard",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.tsx",
|
||||
"private": true,
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./src/platform": "./src/platform.ts",
|
||||
"./src/tailwind.css": "./src/tailwind.css"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc",
|
||||
"compile": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsx bundle.ts",
|
||||
"dev": "vite",
|
||||
"start": "tsx start.ts",
|
||||
@ -16,18 +26,25 @@
|
||||
"test:e2e-and-log": "npm run test:e2e || npx tsx log-screenshot-diffs.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-amplify/auth": "^5.6.5",
|
||||
"@aws-amplify/core": "^5.8.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@heroicons/react": "^2.0.15",
|
||||
"@sentry/react": "^7.74.0",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-time": "^1.0.0",
|
||||
"enso-common": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.7.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"ts-results": "^3.3.0",
|
||||
"validator": "^13.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@fast-check/vitest": "^0.0.8",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||
"@playwright/experimental-ct-react": "^1.40.0",
|
||||
"@playwright/test": "^1.40.0",
|
||||
@ -37,11 +54,12 @@
|
||||
"@types/validator": "^13.11.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"chalk": "^5.3.0",
|
||||
"enso-authentication": "^1.0.0",
|
||||
"enso-chat": "git://github.com/enso-org/enso-bot",
|
||||
"enso-content": "^1.0.0",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-inline-image": "^0.0.9",
|
||||
"esbuild-plugin-time": "^1.0.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-jsdoc": "^46.8.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
@ -50,6 +68,7 @@
|
||||
"postcss": "^8.4.29",
|
||||
"react-toastify": "^9.1.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"ts-plugin-namespace-auto-import": "^1.0.0",
|
||||
"tsx": "^3.12.6",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
@ -59,5 +78,9 @@
|
||||
"@esbuild/darwin-x64": "^0.17.15",
|
||||
"@esbuild/linux-x64": "^0.17.15",
|
||||
"@esbuild/windows-x64": "^0.17.15"
|
||||
},
|
||||
"overrides": {
|
||||
"@aws-amplify/auth": "../_IGNORED_",
|
||||
"react-native-url-polyfill": "../_IGNORED_"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
/** @file Playwright component testing configuration. */
|
||||
import * as componentTesting from '@playwright/experimental-ct-react'
|
||||
|
||||
import vitePluginYaml from '@modyfi/vite-plugin-yaml'
|
||||
import * as componentTesting from '@playwright/experimental-ct-react'
|
||||
|
||||
// This is an autogenerated file.
|
||||
/* eslint-disable @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-magic-numbers */
|
||||
|
@ -33,61 +33,35 @@
|
||||
* signed up but who have not completed email verification or set a username. The remaining
|
||||
* {@link router.Route}s require fully authenticated users (c.f.
|
||||
* {@link authProvider.FullUserSession}). */
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import * as router from 'react-router-dom'
|
||||
import * as toastify from 'react-toastify'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import * as authServiceModule from '../authentication/service'
|
||||
import type * as backend from '../dashboard/backend'
|
||||
import * as hooks from '../hooks'
|
||||
import * as localBackend from '../dashboard/localBackend'
|
||||
import * as shortcutsModule from '../dashboard/shortcuts'
|
||||
|
||||
import * as authProvider from '../authentication/providers/auth'
|
||||
import * as backendProvider from '../providers/backend'
|
||||
import * as localStorageProvider from '../providers/localStorage'
|
||||
import * as loggerProvider from '../providers/logger'
|
||||
import * as modalProvider from '../providers/modal'
|
||||
import * as sessionProvider from '../authentication/providers/session'
|
||||
import * as shortcutsProvider from '../providers/shortcuts'
|
||||
|
||||
import ConfirmRegistration from '../authentication/components/confirmRegistration'
|
||||
import Dashboard from '../dashboard/components/dashboard'
|
||||
import EnterOfflineMode from '../authentication/components/enterOfflineMode'
|
||||
import ForgotPassword from '../authentication/components/forgotPassword'
|
||||
import Login from '../authentication/components/login'
|
||||
import Registration from '../authentication/components/registration'
|
||||
import ResetPassword from '../authentication/components/resetPassword'
|
||||
import SetUsername from '../authentication/components/setUsername'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** Path to the root of the app (i.e., the Cloud dashboard). */
|
||||
export const DASHBOARD_PATH = '/'
|
||||
/** Path to the login page. */
|
||||
export const LOGIN_PATH = '/login'
|
||||
/** Path to the registration page. */
|
||||
export const REGISTRATION_PATH = '/registration'
|
||||
/** Path to the confirm registration page. */
|
||||
export const CONFIRM_REGISTRATION_PATH = '/confirmation'
|
||||
/** Path to the forgot password page. */
|
||||
export const FORGOT_PASSWORD_PATH = '/forgot-password'
|
||||
/** Path to the reset password page. */
|
||||
export const RESET_PASSWORD_PATH = '/password-reset'
|
||||
/** Path to the set username page. */
|
||||
export const SET_USERNAME_PATH = '/set-username'
|
||||
/** Path to the offline mode entrypoint. */
|
||||
export const ENTER_OFFLINE_MODE_PATH = '/offline'
|
||||
/** A {@link RegExp} matching all paths. */
|
||||
export const ALL_PATHS_REGEX = new RegExp(
|
||||
`(?:${DASHBOARD_PATH}|${LOGIN_PATH}|${REGISTRATION_PATH}|${CONFIRM_REGISTRATION_PATH}|` +
|
||||
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH})$`
|
||||
)
|
||||
import * as appUtils from '#/appUtils'
|
||||
import * as authServiceModule from '#/authentication/service'
|
||||
import * as hooks from '#/hooks'
|
||||
import ConfirmRegistration from '#/pages/authentication/ConfirmRegistration'
|
||||
import EnterOfflineMode from '#/pages/authentication/EnterOfflineMode'
|
||||
import ForgotPassword from '#/pages/authentication/ForgotPassword'
|
||||
import Login from '#/pages/authentication/Login'
|
||||
import Registration from '#/pages/authentication/Registration'
|
||||
import ResetPassword from '#/pages/authentication/ResetPassword'
|
||||
import SetUsername from '#/pages/authentication/SetUsername'
|
||||
import Dashboard from '#/pages/dashboard/Dashboard'
|
||||
import AuthProvider, * as authProvider from '#/providers/AuthProvider'
|
||||
import BackendProvider from '#/providers/BackendProvider'
|
||||
import LocalStorageProvider from '#/providers/LocalStorageProvider'
|
||||
import LoggerProvider from '#/providers/LoggerProvider'
|
||||
import type * as loggerProvider from '#/providers/LoggerProvider'
|
||||
import ModalProvider from '#/providers/ModalProvider'
|
||||
import SessionProvider from '#/providers/SessionProvider'
|
||||
import ShortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import type * as backend from '#/services/backend'
|
||||
import * as localBackend from '#/services/localBackend'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
|
||||
// ======================
|
||||
// === getMainPageUrl ===
|
||||
@ -96,7 +70,7 @@ export const ALL_PATHS_REGEX = new RegExp(
|
||||
/** Returns the URL to the main page. This is the current URL, with the current route removed. */
|
||||
function getMainPageUrl() {
|
||||
const mainPageUrl = new URL(window.location.href)
|
||||
mainPageUrl.pathname = mainPageUrl.pathname.replace(ALL_PATHS_REGEX, '')
|
||||
mainPageUrl.pathname = mainPageUrl.pathname.replace(appUtils.ALL_PATHS_REGEX, '')
|
||||
return mainPageUrl
|
||||
}
|
||||
|
||||
@ -162,14 +136,8 @@ export default function App(props: AppProps) {
|
||||
* because the {@link AppRouter} relies on React hooks, which can't be used in the same React
|
||||
* component as the component that defines the provider. */
|
||||
function AppRouter(props: AppProps) {
|
||||
const {
|
||||
logger,
|
||||
supportsLocalBackend,
|
||||
isAuthenticationDisabled,
|
||||
shouldShowDashboard,
|
||||
onAuthenticated,
|
||||
projectManagerUrl,
|
||||
} = props
|
||||
const { logger, supportsLocalBackend, isAuthenticationDisabled, shouldShowDashboard } = props
|
||||
const { onAuthenticated, projectManagerUrl } = props
|
||||
const navigate = hooks.useNavigate()
|
||||
if (detect.IS_DEV_MODE) {
|
||||
// @ts-expect-error This is used exclusively for debugging.
|
||||
@ -213,58 +181,63 @@ function AppRouter(props: AppProps) {
|
||||
<React.Fragment>
|
||||
{/* Login & registration pages are visible to unauthenticated users. */}
|
||||
<router.Route element={<authProvider.GuestLayout />}>
|
||||
<router.Route path={REGISTRATION_PATH} element={<Registration />} />
|
||||
<router.Route path={appUtils.REGISTRATION_PATH} element={<Registration />} />
|
||||
<router.Route
|
||||
path={LOGIN_PATH}
|
||||
path={appUtils.LOGIN_PATH}
|
||||
element={<Login supportsLocalBackend={supportsLocalBackend} />}
|
||||
/>
|
||||
</router.Route>
|
||||
{/* Protected pages are visible to authenticated users. */}
|
||||
<router.Route element={<authProvider.ProtectedLayout />}>
|
||||
<router.Route
|
||||
path={DASHBOARD_PATH}
|
||||
path={appUtils.DASHBOARD_PATH}
|
||||
element={shouldShowDashboard && <Dashboard {...props} />}
|
||||
/>
|
||||
</router.Route>
|
||||
{/* Semi-protected pages are visible to users currently registering. */}
|
||||
<router.Route element={<authProvider.SemiProtectedLayout />}>
|
||||
<router.Route path={SET_USERNAME_PATH} element={<SetUsername />} />
|
||||
<router.Route path={appUtils.SET_USERNAME_PATH} element={<SetUsername />} />
|
||||
</router.Route>
|
||||
{/* Other pages are visible to unauthenticated and authenticated users. */}
|
||||
<router.Route path={CONFIRM_REGISTRATION_PATH} element={<ConfirmRegistration />} />
|
||||
<router.Route path={FORGOT_PASSWORD_PATH} element={<ForgotPassword />} />
|
||||
<router.Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
|
||||
<router.Route path={ENTER_OFFLINE_MODE_PATH} element={<EnterOfflineMode />} />
|
||||
<router.Route
|
||||
path={appUtils.CONFIRM_REGISTRATION_PATH}
|
||||
element={<ConfirmRegistration />}
|
||||
/>
|
||||
<router.Route path={appUtils.FORGOT_PASSWORD_PATH} element={<ForgotPassword />} />
|
||||
<router.Route path={appUtils.RESET_PASSWORD_PATH} element={<ResetPassword />} />
|
||||
<router.Route
|
||||
path={appUtils.ENTER_OFFLINE_MODE_PATH}
|
||||
element={<EnterOfflineMode />}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</router.Routes>
|
||||
)
|
||||
/** {@link backendProvider.BackendProvider} depends on
|
||||
* {@link localStorageProvider.LocalStorageProvider}. */
|
||||
/** {@link BackendProvider} depends on {@link LocalStorageProvider}. */
|
||||
return (
|
||||
<loggerProvider.LoggerProvider logger={logger}>
|
||||
<sessionProvider.SessionProvider
|
||||
<LoggerProvider logger={logger}>
|
||||
<SessionProvider
|
||||
mainPageUrl={mainPageUrl}
|
||||
userSession={userSession}
|
||||
registerAuthEventListener={registerAuthEventListener}
|
||||
>
|
||||
<localStorageProvider.LocalStorageProvider>
|
||||
<backendProvider.BackendProvider initialBackend={initialBackend}>
|
||||
<authProvider.AuthProvider
|
||||
<LocalStorageProvider>
|
||||
<BackendProvider initialBackend={initialBackend}>
|
||||
<AuthProvider
|
||||
shouldStartInOfflineMode={isAuthenticationDisabled}
|
||||
supportsLocalBackend={supportsLocalBackend}
|
||||
authService={authService}
|
||||
onAuthenticated={onAuthenticated}
|
||||
projectManagerUrl={projectManagerUrl}
|
||||
>
|
||||
<modalProvider.ModalProvider>
|
||||
<shortcutsProvider.ShortcutsProvider shortcuts={shortcuts}>
|
||||
<ModalProvider>
|
||||
<ShortcutsProvider shortcuts={shortcuts}>
|
||||
{routes}
|
||||
</shortcutsProvider.ShortcutsProvider>
|
||||
</modalProvider.ModalProvider>
|
||||
</authProvider.AuthProvider>
|
||||
</backendProvider.BackendProvider>
|
||||
</localStorageProvider.LocalStorageProvider>
|
||||
</sessionProvider.SessionProvider>
|
||||
</loggerProvider.LoggerProvider>
|
||||
</ShortcutsProvider>
|
||||
</ModalProvider>
|
||||
</AuthProvider>
|
||||
</BackendProvider>
|
||||
</LocalStorageProvider>
|
||||
</SessionProvider>
|
||||
</LoggerProvider>
|
||||
)
|
||||
}
|
27
app/ide-desktop/lib/dashboard/src/appUtils.tsx
Normal file
27
app/ide-desktop/lib/dashboard/src/appUtils.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
/** @file Constants related to the application root component. */
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** Path to the root of the app (i.e., the Cloud dashboard). */
|
||||
export const DASHBOARD_PATH = '/'
|
||||
/** Path to the login page. */
|
||||
export const LOGIN_PATH = '/login'
|
||||
/** Path to the registration page. */
|
||||
export const REGISTRATION_PATH = '/registration'
|
||||
/** Path to the confirm registration page. */
|
||||
export const CONFIRM_REGISTRATION_PATH = '/confirmation'
|
||||
/** Path to the forgot password page. */
|
||||
export const FORGOT_PASSWORD_PATH = '/forgot-password'
|
||||
/** Path to the reset password page. */
|
||||
export const RESET_PASSWORD_PATH = '/password-reset'
|
||||
/** Path to the set username page. */
|
||||
export const SET_USERNAME_PATH = '/set-username'
|
||||
/** Path to the offline mode entrypoint. */
|
||||
export const ENTER_OFFLINE_MODE_PATH = '/offline'
|
||||
/** A {@link RegExp} matching all paths. */
|
||||
export const ALL_PATHS_REGEX = new RegExp(
|
||||
`(?:${DASHBOARD_PATH}|${LOGIN_PATH}|${REGISTRATION_PATH}|${CONFIRM_REGISTRATION_PATH}|` +
|
||||
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH})$`
|
||||
)
|
@ -35,8 +35,8 @@ import * as results from 'ts-results'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import * as config from './config'
|
||||
import type * as loggerProvider from '../providers/logger'
|
||||
import * as config from '#/authentication/config'
|
||||
import type * as loggerProvider from '#/providers/LoggerProvider'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
@ -10,7 +10,7 @@
|
||||
* pools, Amplify must be configured prior to use. This file defines all the information needed to
|
||||
* connect to and use these pools. */
|
||||
|
||||
import * as newtype from '../newtype'
|
||||
import * as newtype from '#/utilities/newtype'
|
||||
|
||||
// =============
|
||||
// === Types ===
|
@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "enso-authentication",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.tsx",
|
||||
"exports": {
|
||||
".": "./src/index.tsx",
|
||||
"./src/platform": "./src/platform.ts",
|
||||
"./tailwind.css": "./tailwind.css"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-amplify/auth": "^5.6.5",
|
||||
"@aws-amplify/core": "^5.8.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"enso-common": "^1.0.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"ts-results": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@aws-amplify/auth": "../_IGNORED_",
|
||||
"react-native-url-polyfill": "../_IGNORED_"
|
||||
}
|
||||
}
|
@ -6,12 +6,12 @@ import * as amplify from '@aws-amplify/auth'
|
||||
import * as common from 'enso-common'
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import * as app from '../components/app'
|
||||
import * as auth from './config'
|
||||
import * as cognito from './cognito'
|
||||
import * as config from '../config'
|
||||
import * as listen from './listen'
|
||||
import type * as loggerProvider from '../providers/logger'
|
||||
import * as appUtils from '#/appUtils'
|
||||
import * as cognito from '#/authentication/cognito'
|
||||
import * as auth from '#/authentication/config'
|
||||
import * as listen from '#/authentication/listen'
|
||||
import type * as loggerProvider from '#/providers/LoggerProvider'
|
||||
import * as config from '#/utilities/config'
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
@ -208,7 +208,7 @@ function setDeepLinkHandler(logger: loggerProvider.Logger, navigate: (url: strin
|
||||
/** If the user is being redirected after clicking the registration confirmation link in their
|
||||
* email, then the URL will be for the confirmation page path. */
|
||||
case CONFIRM_REGISTRATION_PATHNAME: {
|
||||
const redirectUrl = `${app.CONFIRM_REGISTRATION_PATH}${parsedUrl.search}`
|
||||
const redirectUrl = `${appUtils.CONFIRM_REGISTRATION_PATH}${parsedUrl.search}`
|
||||
navigate(redirectUrl)
|
||||
break
|
||||
}
|
||||
@ -219,7 +219,7 @@ function setDeepLinkHandler(logger: loggerProvider.Logger, navigate: (url: strin
|
||||
case SIGN_IN_PATHNAME:
|
||||
/** If the user is being redirected after a sign-out, then no query args will be present. */
|
||||
if (parsedUrl.search === '') {
|
||||
navigate(app.LOGIN_PATH)
|
||||
navigate(appUtils.LOGIN_PATH)
|
||||
} else {
|
||||
handleAuthResponse(url)
|
||||
}
|
||||
@ -227,16 +227,16 @@ function setDeepLinkHandler(logger: loggerProvider.Logger, navigate: (url: strin
|
||||
/** If the user is being redirected after finishing the password reset flow, then the URL will
|
||||
* be for the login page. */
|
||||
case LOGIN_PATHNAME:
|
||||
navigate(app.LOGIN_PATH)
|
||||
navigate(appUtils.LOGIN_PATH)
|
||||
break
|
||||
case REGISTRATION_PATHNAME:
|
||||
navigate(app.REGISTRATION_PATH + parsedUrl.search)
|
||||
navigate(`${appUtils.REGISTRATION_PATH}${parsedUrl.search}`)
|
||||
break
|
||||
/** If the user is being redirected from a password reset email, then we need to navigate to
|
||||
* the password reset page, with the verification code and email passed in the URL s-o they can
|
||||
* be filled in automatically. */
|
||||
case app.RESET_PASSWORD_PATH: {
|
||||
const resetPasswordRedirectUrl = app.RESET_PASSWORD_PATH + parsedUrl.search
|
||||
case appUtils.RESET_PASSWORD_PATH: {
|
||||
const resetPasswordRedirectUrl = `${appUtils.RESET_PASSWORD_PATH}${parsedUrl.search}`
|
||||
navigate(resetPasswordRedirectUrl)
|
||||
break
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/** @file Tests for `error.ts`. */
|
||||
import * as v from 'vitest'
|
||||
|
||||
import * as errorModule from '../error'
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
const MESSAGE = 'A custom error message.'
|
||||
|
||||
v.test.each([
|
||||
{ error: new Error(MESSAGE), message: MESSAGE },
|
||||
{ error: { message: 'a' }, message: 'a' },
|
||||
{ error: MESSAGE, message: null },
|
||||
{ error: {}, message: null },
|
||||
{ error: null, message: null },
|
||||
])('`error.tryGetMessage`', ({ error, message }) => {
|
||||
v.expect(errorModule.tryGetMessage<unknown>(error)).toBe(message)
|
||||
})
|
@ -1,536 +0,0 @@
|
||||
/** @file Column types and column display modes. */
|
||||
import * as React from 'react'
|
||||
|
||||
import AccessedByProjectsIcon from 'enso-assets/accessed_by_projects.svg'
|
||||
import AccessedDataIcon from 'enso-assets/accessed_data.svg'
|
||||
import DocsIcon from 'enso-assets/docs.svg'
|
||||
import PeopleIcon from 'enso-assets/people.svg'
|
||||
import Plus2Icon from 'enso-assets/plus2.svg'
|
||||
import SortAscendingIcon from 'enso-assets/sort_ascending.svg'
|
||||
import SortDescendingIcon from 'enso-assets/sort_descending.svg'
|
||||
import TagIcon from 'enso-assets/tag.svg'
|
||||
import TimeIcon from 'enso-assets/time.svg'
|
||||
|
||||
import * as assetEvent from './events/assetEvent'
|
||||
import * as assetQuery from '../assetQuery'
|
||||
import type * as assetTreeNode from './assetTreeNode'
|
||||
import * as authProvider from '../authentication/providers/auth'
|
||||
import * as backendModule from './backend'
|
||||
import * as backendProvider from '../providers/backend'
|
||||
import * as dateTime from './dateTime'
|
||||
import * as hooks from '../hooks'
|
||||
import * as modalProvider from '../providers/modal'
|
||||
import * as object from '../object'
|
||||
import * as permissions from './permissions'
|
||||
import * as shortcuts from './shortcuts'
|
||||
import * as sorting from './sorting'
|
||||
import type * as tableColumn from './components/tableColumn'
|
||||
import * as uniqueString from '../uniqueString'
|
||||
|
||||
import type * as assetsTable from './components/assetsTable'
|
||||
import * as categorySwitcher from './components/categorySwitcher'
|
||||
import Label, * as labelModule from './components/label'
|
||||
import AssetNameColumn from './components/assetNameColumn'
|
||||
import ContextMenu from './components/contextMenu'
|
||||
import ContextMenus from './components/contextMenus'
|
||||
import ManageLabelsModal from './components/manageLabelsModal'
|
||||
import ManagePermissionsModal from './components/managePermissionsModal'
|
||||
import MenuEntry from './components/menuEntry'
|
||||
import PermissionDisplay from './components/permissionDisplay'
|
||||
import SvgMask from '../authentication/components/svgMask'
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
// =============
|
||||
|
||||
/** Determines which columns are visible. */
|
||||
export enum ColumnDisplayMode {
|
||||
/** Show only columns which are ready for release. */
|
||||
release = 'release',
|
||||
/** Show all columns. */
|
||||
all = 'all',
|
||||
/** Show only name and metadata. */
|
||||
compact = 'compact',
|
||||
/** Show only columns relevant to documentation editors. */
|
||||
docs = 'docs',
|
||||
/** Show only name, metadata, and configuration options. */
|
||||
settings = 'settings',
|
||||
}
|
||||
|
||||
/** Column type. */
|
||||
export enum Column {
|
||||
name = 'name',
|
||||
modified = 'modified',
|
||||
sharedWith = 'shared-with',
|
||||
labels = 'tags',
|
||||
accessedByProjects = 'accessed-by-projects',
|
||||
accessedData = 'accessed-data',
|
||||
docs = 'docs',
|
||||
}
|
||||
|
||||
/** Columns that can be toggled between visible and hidden. */
|
||||
export type ExtraColumn = (typeof EXTRA_COLUMNS)[number]
|
||||
|
||||
/** Columns that can be used as a sort column. */
|
||||
export type SortableColumn = Column.modified | Column.name
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The list of extra columns, in order. */
|
||||
// This MUST be `as const`, to generate the `ExtraColumn` type above.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export const EXTRA_COLUMNS = [
|
||||
Column.labels,
|
||||
Column.accessedByProjects,
|
||||
Column.accessedData,
|
||||
Column.docs,
|
||||
] as const
|
||||
|
||||
export const EXTRA_COLUMN_IMAGES: Record<ExtraColumn, string> = {
|
||||
[Column.labels]: TagIcon,
|
||||
[Column.accessedByProjects]: AccessedByProjectsIcon,
|
||||
[Column.accessedData]: AccessedDataIcon,
|
||||
[Column.docs]: DocsIcon,
|
||||
}
|
||||
|
||||
/** English names for every column except for the name column. */
|
||||
export const COLUMN_NAME: Record<Column, string> = {
|
||||
[Column.name]: 'Name',
|
||||
[Column.modified]: 'Modified',
|
||||
[Column.sharedWith]: 'Shared with',
|
||||
[Column.labels]: 'Labels',
|
||||
[Column.accessedByProjects]: 'Accessed by projects',
|
||||
[Column.accessedData]: 'Accessed data',
|
||||
[Column.docs]: 'Docs',
|
||||
} as const
|
||||
|
||||
const COLUMN_CSS_CLASSES =
|
||||
'text-left bg-clip-padding border-transparent border-l-2 border-r-2 last:border-r-0'
|
||||
const NORMAL_COLUMN_CSS_CLASSES = `px-2 last:rounded-r-full last:w-full ${COLUMN_CSS_CLASSES}`
|
||||
|
||||
/** CSS classes for every column. */
|
||||
export const COLUMN_CSS_CLASS: Record<Column, string> = {
|
||||
[Column.name]: `rounded-rows-skip-level min-w-61.25 p-0 border-l-0 ${COLUMN_CSS_CLASSES}`,
|
||||
[Column.modified]: `min-w-33.25 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.sharedWith]: `min-w-40 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.labels]: `min-w-80 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.accessedByProjects]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.accessedData]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.docs]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
} as const
|
||||
|
||||
/** {@link tableColumn.TableColumnProps} for an unknown variant of {@link backendModule.Asset}. */
|
||||
export type AssetColumnProps = tableColumn.TableColumnProps<
|
||||
assetTreeNode.AssetTreeNode,
|
||||
assetsTable.AssetsTableState,
|
||||
assetsTable.AssetRowState,
|
||||
backendModule.AssetId
|
||||
>
|
||||
|
||||
// =====================
|
||||
// === getColumnList ===
|
||||
// =====================
|
||||
|
||||
/** Return the full list of columns given the relevant current state. */
|
||||
export function getColumnList(
|
||||
backendType: backendModule.BackendType,
|
||||
extraColumns: Set<ExtraColumn>
|
||||
) {
|
||||
switch (backendType) {
|
||||
case backendModule.BackendType.local: {
|
||||
return [Column.name, Column.modified]
|
||||
}
|
||||
case backendModule.BackendType.remote: {
|
||||
return [
|
||||
Column.name,
|
||||
Column.modified,
|
||||
Column.sharedWith,
|
||||
...EXTRA_COLUMNS.filter(column => extraColumns.has(column)),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// === LastModifiedColumn ===
|
||||
// ==========================
|
||||
|
||||
/** A column displaying the time at which the asset was last modified. */
|
||||
function LastModifiedColumn(props: AssetColumnProps) {
|
||||
return <>{dateTime.formatDateTime(new Date(props.item.item.modifiedAt))}</>
|
||||
}
|
||||
|
||||
// ========================
|
||||
// === SharedWithColumn ===
|
||||
// ========================
|
||||
|
||||
/** The type of the `state` prop of a {@link SharedWithColumn}. */
|
||||
interface SharedWithColumnStateProp {
|
||||
category: AssetColumnProps['state']['category']
|
||||
dispatchAssetEvent: AssetColumnProps['state']['dispatchAssetEvent']
|
||||
}
|
||||
|
||||
/** Props for a {@link SharedWithColumn}. */
|
||||
export interface SharedWithColumnProps extends Pick<AssetColumnProps, 'item' | 'setItem'> {
|
||||
state: SharedWithColumnStateProp
|
||||
}
|
||||
|
||||
/** A column listing the users with which this asset is shared. */
|
||||
export function SharedWithColumn(props: SharedWithColumnProps) {
|
||||
const {
|
||||
item: { item: asset },
|
||||
setItem,
|
||||
state: { category, dispatchAssetEvent },
|
||||
} = props
|
||||
const session = authProvider.useNonPartialUserSession()
|
||||
const { setModal } = modalProvider.useSetModal()
|
||||
const self = asset.permissions?.find(
|
||||
permission => permission.user.user_email === session.organization?.email
|
||||
)
|
||||
const managesThisAsset =
|
||||
category !== categorySwitcher.Category.trash &&
|
||||
(self?.permission === permissions.PermissionAction.own ||
|
||||
self?.permission === permissions.PermissionAction.admin)
|
||||
const setAsset = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<backendModule.AnyAsset>) => {
|
||||
setItem(oldItem =>
|
||||
oldItem.with({
|
||||
item:
|
||||
typeof valueOrUpdater !== 'function'
|
||||
? valueOrUpdater
|
||||
: valueOrUpdater(oldItem.item),
|
||||
})
|
||||
)
|
||||
},
|
||||
[/* should never change */ setItem]
|
||||
)
|
||||
return (
|
||||
<div className="group flex items-center gap-1">
|
||||
{(asset.permissions ?? []).map(user => (
|
||||
<PermissionDisplay key={user.user.pk} action={user.permission}>
|
||||
{user.user.user_name}
|
||||
</PermissionDisplay>
|
||||
))}
|
||||
{managesThisAsset && (
|
||||
<button
|
||||
className="h-4 w-4 invisible pointer-events-none group-hover:visible group-hover:pointer-events-auto"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
setModal(
|
||||
<ManagePermissionsModal
|
||||
key={uniqueString.uniqueString()}
|
||||
item={asset}
|
||||
setItem={setAsset}
|
||||
self={self}
|
||||
eventTarget={event.currentTarget}
|
||||
doRemoveSelf={() => {
|
||||
dispatchAssetEvent({
|
||||
type: assetEvent.AssetEventType.removeSelf,
|
||||
id: asset.id,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img className="w-4.5 h-4.5" src={Plus2Icon} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ====================
|
||||
// === LabelsColumn ===
|
||||
// ====================
|
||||
|
||||
/** A column listing the labels on this asset. */
|
||||
function LabelsColumn(props: AssetColumnProps) {
|
||||
const {
|
||||
item: { item: asset },
|
||||
setItem,
|
||||
state: { category, labels, setQuery, deletedLabelNames, doCreateLabel },
|
||||
rowState: { temporarilyAddedLabels, temporarilyRemovedLabels },
|
||||
} = props
|
||||
const session = authProvider.useNonPartialUserSession()
|
||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
const self = asset.permissions?.find(
|
||||
permission => permission.user.user_email === session.organization?.email
|
||||
)
|
||||
const managesThisAsset =
|
||||
category !== categorySwitcher.Category.trash &&
|
||||
(self?.permission === permissions.PermissionAction.own ||
|
||||
self?.permission === permissions.PermissionAction.admin)
|
||||
const setAsset = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<backendModule.AnyAsset>) => {
|
||||
setItem(oldItem =>
|
||||
oldItem.with({
|
||||
item:
|
||||
typeof valueOrUpdater !== 'function'
|
||||
? valueOrUpdater
|
||||
: valueOrUpdater(oldItem.item),
|
||||
})
|
||||
)
|
||||
},
|
||||
[/* should never change */ setItem]
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
{(asset.labels ?? [])
|
||||
.filter(label => !deletedLabelNames.has(label))
|
||||
.map(label => (
|
||||
<Label
|
||||
key={label}
|
||||
title="Right click to remove label."
|
||||
color={labels.get(label)?.color ?? labelModule.DEFAULT_LABEL_COLOR}
|
||||
active={!temporarilyRemovedLabels.has(label)}
|
||||
disabled={temporarilyRemovedLabels.has(label)}
|
||||
negated={temporarilyRemovedLabels.has(label)}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const doDelete = () => {
|
||||
unsetModal()
|
||||
setAsset(oldAsset => {
|
||||
const newLabels =
|
||||
oldAsset.labels?.filter(oldLabel => oldLabel !== label) ??
|
||||
[]
|
||||
void backend
|
||||
.associateTag(asset.id, newLabels, asset.title)
|
||||
.catch(error => {
|
||||
toastAndLog(null, error)
|
||||
setAsset(oldAsset2 =>
|
||||
oldAsset2.labels?.some(
|
||||
oldLabel => oldLabel === label
|
||||
) === true
|
||||
? oldAsset2
|
||||
: object.merge(oldAsset2, {
|
||||
labels: [
|
||||
...(oldAsset2.labels ?? []),
|
||||
label,
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
return {
|
||||
...oldAsset,
|
||||
labels: newLabels,
|
||||
}
|
||||
})
|
||||
}
|
||||
setModal(
|
||||
<ContextMenus key={`label-${label}`} event={event}>
|
||||
<ContextMenu>
|
||||
<MenuEntry
|
||||
action={shortcuts.KeyboardAction.delete}
|
||||
doAction={doDelete}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</ContextMenus>
|
||||
)
|
||||
}}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setQuery(oldQuery =>
|
||||
assetQuery.toggleLabel(oldQuery, label, event.shiftKey)
|
||||
)
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
))}
|
||||
{...[...temporarilyAddedLabels]
|
||||
.filter(label => asset.labels?.includes(label) !== true)
|
||||
.map(label => (
|
||||
<Label
|
||||
disabled
|
||||
key={label}
|
||||
color={labels.get(label)?.color ?? labelModule.DEFAULT_LABEL_COLOR}
|
||||
className="pointer-events-none"
|
||||
onClick={() => {}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
))}
|
||||
{managesThisAsset && (
|
||||
<button
|
||||
className={`h-4 w-4 ${isHovered ? '' : 'invisible pointer-events-none'}`}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
setModal(
|
||||
<ManageLabelsModal
|
||||
key={uniqueString.uniqueString()}
|
||||
item={asset}
|
||||
setItem={setAsset}
|
||||
allLabels={labels}
|
||||
doCreateLabel={doCreateLabel}
|
||||
eventTarget={event.currentTarget}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img className="w-4.5 h-4.5" src={Plus2Icon} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** A column listing the users with which this asset is shared. */
|
||||
export function DocsColumn(props: AssetColumnProps) {
|
||||
const {
|
||||
item: { item: asset },
|
||||
} = props
|
||||
return <div className="flex items-center gap-1">{asset.description}</div>
|
||||
}
|
||||
|
||||
// =========================
|
||||
// === PlaceholderColumn ===
|
||||
// =========================
|
||||
|
||||
/** A placeholder component for columns which do not yet have corresponding data to display. */
|
||||
function PlaceholderColumn() {
|
||||
return <></>
|
||||
}
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The corresponding icon URL for each {@link sorting.SortDirection}. */
|
||||
const SORT_ICON: Record<sorting.SortDirection, string> = {
|
||||
[sorting.SortDirection.ascending]: SortAscendingIcon,
|
||||
[sorting.SortDirection.descending]: SortDescendingIcon,
|
||||
}
|
||||
|
||||
export const COLUMN_HEADING: Record<
|
||||
Column,
|
||||
(props: tableColumn.TableColumnHeadingProps<assetsTable.AssetsTableState>) => JSX.Element
|
||||
> = {
|
||||
[Column.name]: props => {
|
||||
const {
|
||||
state: { sortColumn, setSortColumn, sortDirection, setSortDirection },
|
||||
} = props
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
const isSortActive = sortColumn === Column.name && sortDirection != null
|
||||
return (
|
||||
<div
|
||||
className="flex items-center cursor-pointer gap-2 pt-1 pb-1.5"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (sortColumn === Column.name) {
|
||||
setSortDirection(sorting.NEXT_SORT_DIRECTION[sortDirection ?? 'null'])
|
||||
} else {
|
||||
setSortColumn(Column.name)
|
||||
setSortDirection(sorting.SortDirection.ascending)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="leading-144.5 h-6 py-0.5">{COLUMN_NAME[Column.name]}</span>
|
||||
<img
|
||||
src={isSortActive ? SORT_ICON[sortDirection] : SortAscendingIcon}
|
||||
className={isSortActive ? '' : isHovered ? 'opacity-50' : 'opacity-0'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[Column.modified]: props => {
|
||||
const {
|
||||
state: { sortColumn, setSortColumn, sortDirection, setSortDirection },
|
||||
} = props
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
const isSortActive = sortColumn === Column.modified && sortDirection != null
|
||||
return (
|
||||
<div
|
||||
className="flex items-center cursor-pointer gap-2"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (sortColumn === Column.modified) {
|
||||
setSortDirection(sorting.NEXT_SORT_DIRECTION[sortDirection ?? 'null'])
|
||||
} else {
|
||||
setSortColumn(Column.modified)
|
||||
setSortDirection(sorting.SortDirection.ascending)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SvgMask src={TimeIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">{COLUMN_NAME[Column.modified]}</span>
|
||||
<img
|
||||
src={isSortActive ? SORT_ICON[sortDirection] : SortAscendingIcon}
|
||||
className={isSortActive ? '' : isHovered ? 'opacity-50' : 'opacity-0'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[Column.sharedWith]: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={PeopleIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">{COLUMN_NAME[Column.sharedWith]}</span>
|
||||
</div>
|
||||
),
|
||||
[Column.labels]: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={TagIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">{COLUMN_NAME[Column.labels]}</span>
|
||||
</div>
|
||||
),
|
||||
[Column.accessedByProjects]: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={AccessedByProjectsIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{COLUMN_NAME[Column.accessedByProjects]}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
[Column.accessedData]: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={AccessedDataIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">{COLUMN_NAME[Column.accessedData]}</span>
|
||||
</div>
|
||||
),
|
||||
[Column.docs]: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={DocsIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">{COLUMN_NAME[Column.docs]}</span>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
/** React components for every column except for the name column. */
|
||||
// This is not a React component even though it contains JSX.
|
||||
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unused-vars
|
||||
export const COLUMN_RENDERER: Record<Column, (props: AssetColumnProps) => JSX.Element> = {
|
||||
[Column.name]: AssetNameColumn,
|
||||
[Column.modified]: LastModifiedColumn,
|
||||
[Column.sharedWith]: SharedWithColumn,
|
||||
[Column.labels]: LabelsColumn,
|
||||
[Column.accessedByProjects]: PlaceholderColumn,
|
||||
[Column.accessedData]: PlaceholderColumn,
|
||||
[Column.docs]: DocsColumn,
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/** @file A spinning arc that animates using the `dasharray-<percentage>` custom Tailwind
|
||||
* classes. */
|
||||
import * as React from 'react'
|
||||
|
||||
// ===============
|
||||
// === Spinner ===
|
||||
// ===============
|
||||
|
||||
/** The state of the spinner. It should go from initial, to loading, to done. */
|
||||
export enum SpinnerState {
|
||||
initial = 'initial',
|
||||
loadingSlow = 'loading-slow',
|
||||
loadingMedium = 'loading-medium',
|
||||
loadingFast = 'loading-fast',
|
||||
done = 'done',
|
||||
}
|
||||
|
||||
export const SPINNER_CSS_CLASSES: Record<SpinnerState, string> = {
|
||||
[SpinnerState.initial]: 'dasharray-5 ease-linear',
|
||||
[SpinnerState.loadingSlow]: 'dasharray-75 duration-90000 ease-linear',
|
||||
[SpinnerState.loadingMedium]: 'dasharray-75 duration-5000 ease-linear',
|
||||
[SpinnerState.loadingFast]: 'dasharray-75 duration-1000 ease-linear',
|
||||
[SpinnerState.done]: 'dasharray-100 duration-1000 ease-in',
|
||||
} as const
|
||||
|
||||
/** Props for a {@link Spinner}. */
|
||||
export interface SpinnerProps {
|
||||
size: number
|
||||
state: SpinnerState
|
||||
}
|
||||
|
||||
/** A spinning arc that animates using the `dasharray-<percentage>` custom Tailwind classes. */
|
||||
export default function Spinner(props: SpinnerProps) {
|
||||
const { size, state } = props
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x={1.5}
|
||||
y={1.5}
|
||||
width={21}
|
||||
height={21}
|
||||
rx={10.5}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={3}
|
||||
className={
|
||||
'animate-spin-ease origin-center transition-stroke-dasharray ' +
|
||||
`pointer-events-none ${SPINNER_CSS_CLASSES[state]}`
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/** @file A spinner that does not expose its {@link spinner.SpinnerState}. */
|
||||
import * as React from 'react'
|
||||
|
||||
import Spinner, * as spinner from './spinner'
|
||||
|
||||
// ========================
|
||||
// === StatelessSpinner ===
|
||||
// ========================
|
||||
|
||||
// This is a re-export, so that the API of this module mirrors that of the `spinner` module.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export { SpinnerState } from './spinner'
|
||||
|
||||
/** Props for a {@link StatelessSpinner}. */
|
||||
export interface StatelessSpinnerProps extends spinner.SpinnerProps {}
|
||||
|
||||
/** A spinner that does not expose its {@link spinner.SpinnerState}. Instead, it begins at
|
||||
* {@link spinner.SpinnerState.initial} and immediately changes to the given state. */
|
||||
export default function StatelessSpinner(props: StatelessSpinnerProps) {
|
||||
const { size, state: rawState } = props
|
||||
const [state, setState] = React.useState(spinner.SpinnerState.initial)
|
||||
|
||||
React.useEffect(() => {
|
||||
setState(rawState)
|
||||
}, [/* should never change */ rawState])
|
||||
|
||||
return <Spinner size={size} state={state} />
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
/** @file Utility functions and constants for sets. */
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
export const EMPTY: ReadonlySet<never> = new Set<never>()
|
@ -1,297 +0,0 @@
|
||||
/** @file Module containing common custom React hooks used throughout out Dashboard. */
|
||||
import * as React from 'react'
|
||||
import * as router from 'react-router'
|
||||
import * as toastify from 'react-toastify'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import * as app from './components/app'
|
||||
import * as auth from './authentication/providers/auth'
|
||||
import * as errorModule from './error'
|
||||
import * as loggerProvider from './providers/logger'
|
||||
|
||||
// ======================
|
||||
// === useToastAndLog ===
|
||||
// ======================
|
||||
|
||||
/** Return a function to send a toast with rendered error message. The same message is also logged
|
||||
* as an error. */
|
||||
export function useToastAndLog() {
|
||||
const logger = loggerProvider.useLogger()
|
||||
return React.useCallback(
|
||||
<T>(
|
||||
messagePrefix: string | null,
|
||||
error?: errorModule.MustNotBeKnown<T>,
|
||||
options?: toastify.ToastOptions
|
||||
) => {
|
||||
const message =
|
||||
error == null
|
||||
? `${messagePrefix ?? ''}.`
|
||||
: // DO NOT explicitly pass the generic parameter anywhere else.
|
||||
// It is only being used here because this function also checks for
|
||||
// `MustNotBeKnown<T>`.
|
||||
`${
|
||||
messagePrefix != null ? messagePrefix + ': ' : ''
|
||||
}${errorModule.getMessageOrToString<unknown>(error)}`
|
||||
const id = toastify.toast.error(message, options)
|
||||
logger.error(message)
|
||||
return id
|
||||
},
|
||||
[/* should never change */ logger]
|
||||
)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// === useAsyncEffect ===
|
||||
// ======================
|
||||
|
||||
/** A React hook for re-rendering a component once an asynchronous call is over.
|
||||
*
|
||||
*This hook will take care of setting an initial value for the component state (so that it can
|
||||
*render immediately), updating the state once the asynchronous call is over (to re-render the
|
||||
*component), and cancelling any in-progress asynchronous calls when the component is unmounted (to
|
||||
*avoid race conditions where "update 1" starts, "update 2" starts and finishes, then "update 1"
|
||||
*finishes and sets the state).
|
||||
*
|
||||
*For further details, see: https://devtrium.com/posts/async-functions-useeffect.
|
||||
*Also see: https://stackoverflow.com/questions/61751728/asynchronous-calls-with-react-usememo.
|
||||
* @param initialValue - The initial value of the state controlled by this hook.
|
||||
* @param asyncEffect - The asynchronous function used to load the state controlled by this hook.
|
||||
* @param deps - The list of dependencies that, when updated, trigger the asynchronous effect.
|
||||
* @returns The current value of the state controlled by this hook. */
|
||||
export function useAsyncEffect<T>(
|
||||
initialValue: T,
|
||||
asyncEffect: (signal: AbortSignal) => Promise<T>,
|
||||
deps?: React.DependencyList
|
||||
): T {
|
||||
const toastAndLog = useToastAndLog()
|
||||
const [value, setValue] = React.useState<T>(initialValue)
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await asyncEffect(controller.signal)
|
||||
if (!controller.signal.aborted) {
|
||||
setValue(result)
|
||||
}
|
||||
} catch (error) {
|
||||
toastAndLog('Error while fetching data', error)
|
||||
}
|
||||
})()
|
||||
/** Cancel any future `setValue` calls. */
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
// This is a wrapper function around `useEffect`, so it has its own `deps` array.
|
||||
// `asyncEffect` is omitted as it always changes - this is intentional.
|
||||
// `logger` is omitted as it should not trigger the effect.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps ?? [])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// ===================
|
||||
// === useNavigate ===
|
||||
// ===================
|
||||
|
||||
/** A wrapper around {@link router.useNavigate} that goes into offline mode when
|
||||
* offline. */
|
||||
export function useNavigate() {
|
||||
const { goOffline } = auth.useAuth()
|
||||
// This function is a wrapper around `router.useNavigate`. It shouldbe the only place where
|
||||
// `router.useNavigate` is used.
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
const originalNavigate = router.useNavigate()
|
||||
|
||||
const navigate: router.NavigateFunction = (...args: [unknown, unknown?]) => {
|
||||
const isOnline = navigator.onLine
|
||||
if (!isOnline) {
|
||||
void goOffline()
|
||||
originalNavigate(app.DASHBOARD_PATH)
|
||||
} else {
|
||||
// This is safe, because the arguments are being passed through transparently.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
originalNavigate(...(args as [never, never?]))
|
||||
}
|
||||
}
|
||||
|
||||
return navigate
|
||||
}
|
||||
|
||||
// =======================
|
||||
// === Reactive Events ===
|
||||
// =======================
|
||||
|
||||
/** A map containing all known event types. Names MUST be chosen carefully to avoid conflicts.
|
||||
* The simplest way to achieve this is by namespacing names using a prefix. */
|
||||
export interface KnownEventsMap {}
|
||||
|
||||
/** A union of all known events. */
|
||||
type KnownEvent = KnownEventsMap[keyof KnownEventsMap]
|
||||
|
||||
/** A wrapper around `useState` that calls `flushSync` after every `setState`.
|
||||
* This is required so that no events are dropped. */
|
||||
export function useEvent<T extends KnownEvent>(): [events: T[], dispatchEvent: (event: T) => void] {
|
||||
const [events, setEvents] = React.useState<T[]>([])
|
||||
React.useEffect(() => {
|
||||
if (events.length !== 0) {
|
||||
// This must run after the current render, but before the next.
|
||||
queueMicrotask(() => {
|
||||
setEvents([])
|
||||
})
|
||||
}
|
||||
}, [events])
|
||||
const dispatchEvent = React.useCallback((event: T) => {
|
||||
setEvents(oldEvents => [...oldEvents, event])
|
||||
}, [])
|
||||
return [events, dispatchEvent]
|
||||
}
|
||||
|
||||
/** A wrapper around `useEffect` that has `event` as its sole dependency. */
|
||||
export function useEventHandler<T extends KnownEvent>(
|
||||
events: T[],
|
||||
effect: (event: T) => Promise<void> | void
|
||||
) {
|
||||
let hasEffectRun = false
|
||||
React.useLayoutEffect(() => {
|
||||
if (detect.IS_DEV_MODE) {
|
||||
if (hasEffectRun) {
|
||||
// This is the second time this event is being run in React Strict Mode.
|
||||
// Event handlers are not supposed to be idempotent, so it is a mistake to execute it
|
||||
// a second time.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return
|
||||
} else {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
hasEffectRun = true
|
||||
}
|
||||
}
|
||||
void (async () => {
|
||||
for (const event of events) {
|
||||
await effect(event)
|
||||
}
|
||||
})()
|
||||
}, [events])
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// === Debug wrappers for built-in hooks ===
|
||||
// =========================================
|
||||
|
||||
// `console.*` is allowed because these are for debugging purposes only.
|
||||
/* eslint-disable no-restricted-properties */
|
||||
|
||||
// === useDebugState ===
|
||||
|
||||
// `console.*` is allowed because this is for debugging purposes only.
|
||||
/* eslint-disable no-restricted-properties */
|
||||
|
||||
/** A modified `useState` that logs the old and new values when `setState` is called. */
|
||||
export function useDebugState<T>(
|
||||
initialState: T | (() => T),
|
||||
name?: string
|
||||
): [state: T, setState: (valueOrUpdater: React.SetStateAction<T>, source?: string) => void] {
|
||||
const [state, rawSetState] = React.useState(initialState)
|
||||
|
||||
const description = name != null ? `state for '${name}'` : 'state'
|
||||
|
||||
const setState = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<T>, source?: string) => {
|
||||
const fullDescription = `${description}${source != null ? ` from '${source}'` : ''}`
|
||||
rawSetState(oldState => {
|
||||
const newState =
|
||||
typeof valueOrUpdater === 'function'
|
||||
? // This is UNSAFE when `T` is itself a function type,
|
||||
// however React makes the same assumption.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(valueOrUpdater as (prevState: T) => T)(oldState)
|
||||
: valueOrUpdater
|
||||
if (!Object.is(oldState, newState)) {
|
||||
console.group(description)
|
||||
console.trace(description)
|
||||
console.log(`Old ${fullDescription}:`, oldState)
|
||||
console.log(`New ${fullDescription}:`, newState)
|
||||
console.groupEnd()
|
||||
}
|
||||
return newState
|
||||
})
|
||||
},
|
||||
[description]
|
||||
)
|
||||
|
||||
return [state, setState]
|
||||
}
|
||||
|
||||
// === useMonitorDependencies ===
|
||||
|
||||
/** A helper function to log the old and new values of changed dependencies. */
|
||||
function useMonitorDependencies(
|
||||
dependencies: React.DependencyList,
|
||||
description?: string,
|
||||
dependencyDescriptions?: readonly string[]
|
||||
) {
|
||||
const oldDependenciesRef = React.useRef(dependencies)
|
||||
const indicesOfChangedDependencies = dependencies.flatMap((dep, i) =>
|
||||
Object.is(dep, oldDependenciesRef.current[i]) ? [] : [i]
|
||||
)
|
||||
if (indicesOfChangedDependencies.length !== 0) {
|
||||
const descriptionText = description == null ? '' : `for '${description}'`
|
||||
console.group(`dependencies changed${descriptionText}`)
|
||||
for (const i of indicesOfChangedDependencies) {
|
||||
console.group(dependencyDescriptions?.[i] ?? `dependency #${i + 1}`)
|
||||
console.log('old value:', oldDependenciesRef.current[i])
|
||||
console.log('new value:', dependencies[i])
|
||||
console.groupEnd()
|
||||
}
|
||||
console.groupEnd()
|
||||
}
|
||||
oldDependenciesRef.current = dependencies
|
||||
}
|
||||
|
||||
/* eslint-enable no-restricted-properties */
|
||||
|
||||
// === useDebugEffect ===
|
||||
|
||||
/** A modified `useEffect` that logs the old and new values of changed dependencies. */
|
||||
export function useDebugEffect(
|
||||
effect: React.EffectCallback,
|
||||
deps: React.DependencyList,
|
||||
description?: string,
|
||||
dependencyDescriptions?: readonly string[]
|
||||
) {
|
||||
useMonitorDependencies(deps, description, dependencyDescriptions)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
React.useEffect(effect, deps)
|
||||
}
|
||||
|
||||
// === useDebugMemo ===
|
||||
|
||||
/** A modified `useMemo` that logs the old and new values of changed dependencies. */
|
||||
export function useDebugMemo<T>(
|
||||
factory: () => T,
|
||||
deps: React.DependencyList,
|
||||
description?: string,
|
||||
dependencyDescriptions?: readonly string[]
|
||||
) {
|
||||
useMonitorDependencies(deps, description, dependencyDescriptions)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return React.useMemo<T>(factory, deps)
|
||||
}
|
||||
|
||||
// === useDebugCallback ===
|
||||
|
||||
/** A modified `useCallback` that logs the old and new values of changed dependencies. */
|
||||
export function useDebugCallback<T extends (...args: never[]) => unknown>(
|
||||
callback: T,
|
||||
deps: React.DependencyList,
|
||||
description?: string,
|
||||
dependencyDescriptions?: readonly string[]
|
||||
) {
|
||||
useMonitorDependencies(deps, description, dependencyDescriptions)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return React.useCallback<T>(callback, deps)
|
||||
}
|
||||
|
||||
/* eslint-enable no-restricted-properties */
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["../../../types", "."]
|
||||
}
|
@ -73,25 +73,9 @@ export type AutocompleteProps<T> = (
|
||||
|
||||
/** A select menu with a dropdown. */
|
||||
export default function Autocomplete<T>(props: AutocompleteProps<T>) {
|
||||
const {
|
||||
multiple,
|
||||
type = 'text',
|
||||
inputRef: rawInputRef,
|
||||
placeholder,
|
||||
values,
|
||||
setValues,
|
||||
text,
|
||||
setText,
|
||||
autoFocus,
|
||||
items,
|
||||
itemToKey,
|
||||
itemToString,
|
||||
itemsToString,
|
||||
matches,
|
||||
className,
|
||||
inputClassName,
|
||||
optionsClassName,
|
||||
} = props
|
||||
const { multiple, type = 'text', inputRef: rawInputRef, placeholder, values, setValues } = props
|
||||
const { text, setText, autoFocus, items, itemToKey, itemToString, itemsToString } = props
|
||||
const { matches, className, inputClassName, optionsClassName } = props
|
||||
const [isDropdownVisible, setIsDropdownVisible] = React.useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = React.useState<number | null>(null)
|
||||
const valuesSet = React.useMemo(() => new Set(values), [values])
|
@ -1,6 +1,7 @@
|
||||
/** @file A styled button. */
|
||||
import * as React from 'react'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
/** Props for a {@link Button}. */
|
||||
export interface ButtonProps {
|
||||
@ -19,16 +20,8 @@ export interface ButtonProps {
|
||||
|
||||
/** A styled button. */
|
||||
export default function Button(props: ButtonProps) {
|
||||
const {
|
||||
active = false,
|
||||
disabled = false,
|
||||
disabledOpacityClassName,
|
||||
image,
|
||||
alt,
|
||||
error,
|
||||
className,
|
||||
onClick,
|
||||
} = props
|
||||
const { active = false, disabled = false, disabledOpacityClassName, image, alt, error } = props
|
||||
const { className, onClick } = props
|
||||
|
||||
return (
|
||||
<SvgMask
|
@ -1,7 +1,7 @@
|
||||
/** @file A color picker to select from a predetermined list of colors. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backend from '../backend'
|
||||
import * as backend from '#/services/backend'
|
||||
|
||||
/** Props for a {@link ColorPicker}. */
|
||||
export interface ColorPickerProps {
|
@ -1,7 +1,7 @@
|
||||
/** @file A context menu. */
|
||||
import * as React from 'react'
|
||||
|
||||
import Modal from './modal'
|
||||
import Modal from '#/components/Modal'
|
||||
|
||||
// ===================
|
||||
// === ContextMenu ===
|
@ -1,9 +1,9 @@
|
||||
/** @file Modal for confirming delete of any type of asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
|
||||
import Modal from './modal'
|
||||
import Modal from '#/components/Modal'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
@ -4,8 +4,8 @@ import * as React from 'react'
|
||||
import CrossIcon from 'enso-assets/cross.svg'
|
||||
import TickIcon from 'enso-assets/tick.svg'
|
||||
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
|
||||
// ====================
|
||||
// === EditableSpan ===
|
||||
@ -35,7 +35,7 @@ export default function EditableSpan(props: EditableSpanProps) {
|
||||
onCancel,
|
||||
inputPattern,
|
||||
inputTitle,
|
||||
...passthroughProps
|
||||
...passthrough
|
||||
} = props
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
const [isSubmittable, setIsSubmittable] = React.useState(true)
|
||||
@ -89,7 +89,7 @@ export default function EditableSpan(props: EditableSpanProps) {
|
||||
setIsSubmittable(checkSubmittable(event.currentTarget.value))
|
||||
},
|
||||
})}
|
||||
{...passthroughProps}
|
||||
{...passthrough}
|
||||
/>
|
||||
{isSubmittable && (
|
||||
<button type="submit" className="mx-0.5">
|
||||
@ -109,6 +109,6 @@ export default function EditableSpan(props: EditableSpanProps) {
|
||||
</form>
|
||||
)
|
||||
} else {
|
||||
return <span {...passthroughProps}>{children}</span>
|
||||
return <span {...passthrough}>{children}</span>
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
/** @file A fixed-size container for a {@link fontawesome.FontAwesomeIcon FontAwesomeIcon}. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as fontawesome from '@fortawesome/react-fontawesome'
|
||||
import type * as fontawesomeIcons from '@fortawesome/free-brands-svg-icons'
|
||||
import * as fontawesome from '@fortawesome/react-fontawesome'
|
||||
|
||||
// =======================
|
||||
// === FontAwesomeIcon ===
|
@ -4,9 +4,9 @@ import * as React from 'react'
|
||||
import EyeCrossedIcon from 'enso-assets/eye_crossed.svg'
|
||||
import EyeIcon from 'enso-assets/eye.svg'
|
||||
|
||||
import type * as controlledInput from './controlledInput'
|
||||
import ControlledInput from './controlledInput'
|
||||
import SvgIcon from './svgIcon'
|
||||
import type * as controlledInput from '#/components/ControlledInput'
|
||||
import ControlledInput from '#/components/ControlledInput'
|
||||
import SvgIcon from '#/components/SvgIcon'
|
||||
|
||||
// =============
|
||||
// === Input ===
|
||||
@ -29,7 +29,6 @@ export default function Input(props: InputProps) {
|
||||
<SvgIcon src={icon} />
|
||||
<ControlledInput {...passthrough} type={isShowingPassword ? 'text' : props.type} />
|
||||
{props.type === 'password' && allowShowingPassword && (
|
||||
// FIXME:
|
||||
<SvgIcon
|
||||
src={isShowingPassword ? EyeIcon : EyeCrossedIcon}
|
||||
className="cursor-pointer rounded-full"
|
@ -1,8 +1,9 @@
|
||||
/** @file A styled colored link with an icon. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as router from 'react-router-dom'
|
||||
|
||||
import SvgMask from './svgMask'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// ============
|
||||
// === Link ===
|
@ -1,11 +1,11 @@
|
||||
/** @file An entry in a menu. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
|
||||
import KeyboardShortcut from './keyboardShortcut'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import KeyboardShortcut from '#/components/dashboard/keyboardShortcut'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// =================
|
||||
// === MenuEntry ===
|
@ -1,7 +1,7 @@
|
||||
/** @file Base modal component that provides the full-screen element that blocks mouse events. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
|
||||
// =================
|
||||
// === Component ===
|
10
app/ide-desktop/lib/dashboard/src/components/README.md
Normal file
10
app/ide-desktop/lib/dashboard/src/components/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Components
|
||||
|
||||
This folder contains reusable styled components.
|
||||
|
||||
Components designed to take up the full page are located in `pages/`; components
|
||||
that are supposed to be used only once are located in `layouts/`.
|
||||
|
||||
Generic components usable anywhere belong directly in this folder; components
|
||||
only relevant to a specific part of the app belong in a subfolder named after
|
||||
the corresponding section.
|
@ -1,7 +1,7 @@
|
||||
/** @file A spinner that does not expose its {@link spinner.SpinnerState}. */
|
||||
import * as React from 'react'
|
||||
|
||||
import Spinner, * as spinner from './spinner'
|
||||
import Spinner, * as spinner from '#/components/Spinner'
|
||||
|
||||
// ========================
|
||||
// === StatelessSpinner ===
|
||||
@ -9,7 +9,7 @@ import Spinner, * as spinner from './spinner'
|
||||
|
||||
// This is a re-export, so that the API of this module mirrors that of the `spinner` module.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export { SpinnerState } from './spinner'
|
||||
export { SpinnerState } from './Spinner'
|
||||
|
||||
/** Props for a {@link StatelessSpinner}. */
|
||||
export interface StatelessSpinnerProps extends spinner.SpinnerProps {}
|
@ -1,7 +1,7 @@
|
||||
/** @file A styled submit button. */
|
||||
import * as React from 'react'
|
||||
|
||||
import SvgMask from './svgMask'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// ====================
|
||||
// === SubmitButton ===
|
@ -1,7 +1,7 @@
|
||||
/** @file Styled wrapper around SVG images. */
|
||||
import * as React from 'react'
|
||||
|
||||
import SvgMask from './svgMask'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// ===============
|
||||
// === SvgIcon ===
|
@ -3,14 +3,14 @@
|
||||
* being used directly. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as set from '../../set'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as set from '#/utilities/set'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
|
||||
import type * as tableColumn from './tableColumn'
|
||||
import type * as tableRow from './tableRow'
|
||||
import Spinner, * as spinner from './spinner'
|
||||
import TableRow from './tableRow'
|
||||
import Spinner, * as spinner from '#/components/Spinner'
|
||||
import type * as tableColumn from '#/components/TableColumn'
|
||||
import type * as tableRow from '#/components/TableRow'
|
||||
import TableRow from '#/components/TableRow'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -95,29 +95,14 @@ export type TableProps<
|
||||
export default function Table<T, State = never, RowState = never, Key extends string = string>(
|
||||
props: TableProps<T, State, RowState, Key>
|
||||
) {
|
||||
const {
|
||||
rowComponent: RowComponent = TableRow,
|
||||
scrollContainerRef,
|
||||
headerRowRef,
|
||||
footer,
|
||||
items,
|
||||
filter,
|
||||
getKey,
|
||||
selectedKeys: rawSelectedKeys,
|
||||
setSelectedKeys: rawSetSelectedKeys,
|
||||
columns,
|
||||
isLoading,
|
||||
placeholder,
|
||||
onContextMenu,
|
||||
draggableRows,
|
||||
onDragLeave,
|
||||
onRowDragStart,
|
||||
onRowDrag,
|
||||
onRowDragOver,
|
||||
onRowDragEnd,
|
||||
onRowDrop,
|
||||
...rowProps
|
||||
} = props
|
||||
const { rowComponent: RowComponent = TableRow, scrollContainerRef, headerRowRef } = props
|
||||
const { footer, items, filter, getKey } = props
|
||||
const { selectedKeys: rawSelectedKeys, setSelectedKeys: rawSetSelectedKeys, columns } = props
|
||||
const { isLoading, placeholder, onContextMenu, draggableRows, onDragLeave } = props
|
||||
const { onRowDragStart, onRowDrag, onRowDragOver, onRowDragEnd, onRowDrop } = props
|
||||
const { className, initialRowState, state } = props
|
||||
const rowProps = { className, initialRowState, state }
|
||||
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
const [spinnerState, setSpinnerState] = React.useState(spinner.SpinnerState.initial)
|
||||
// This should not be made mutable for the sake of optimization, otherwise its value may
|
@ -1,9 +1,9 @@
|
||||
/** @file A row in a `Table`. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
|
||||
import type * as tableColumn from './tableColumn'
|
||||
import type * as tableColumn from '#/components/TableColumn'
|
||||
|
||||
// =============================
|
||||
// === Partial `Props` types ===
|
@ -4,10 +4,10 @@ import * as React from 'react'
|
||||
import DocsIcon from 'enso-assets/docs.svg'
|
||||
import SettingsIcon from 'enso-assets/settings.svg'
|
||||
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
|
||||
import Button from './button'
|
||||
import Button from '#/components/Button'
|
||||
|
||||
/** Props for an {@link AssetInfoBar}. */
|
||||
export interface AssetInfoBarProps {
|
@ -1,13 +1,13 @@
|
||||
/** @file The icon and name of an {@link backendModule.Asset}. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendModule from '#/services/backend'
|
||||
|
||||
import type * as column from '../column'
|
||||
import ConnectorNameColumn from './connectorNameColumn'
|
||||
import DirectoryNameColumn from './directoryNameColumn'
|
||||
import FileNameColumn from './fileNameColumn'
|
||||
import ProjectNameColumn from './projectNameColumn'
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import ConnectorNameColumn from '#/components/dashboard/ConnectorNameColumn'
|
||||
import DirectoryNameColumn from '#/components/dashboard/DirectoryNameColumn'
|
||||
import FileNameColumn from '#/components/dashboard/FileNameColumn'
|
||||
import ProjectNameColumn from '#/components/dashboard/ProjectNameColumn'
|
||||
|
||||
// =================
|
||||
// === AssetName ===
|
@ -3,29 +3,29 @@ import * as React from 'react'
|
||||
|
||||
import BlankIcon from 'enso-assets/blank.svg'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as dateTime from '../dateTime'
|
||||
import * as download from '../../download'
|
||||
import * as drag from '../drag'
|
||||
import * as errorModule from '../../error'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as indent from '../indent'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as object from '../../object'
|
||||
import * as permissions from '../permissions'
|
||||
import * as set from '../set'
|
||||
import * as visibilityModule from '../visibility'
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import AssetListEventType from '#/events/AssetListEventType'
|
||||
import * as hooks from '#/hooks'
|
||||
import AssetContextMenu from '#/layouts/dashboard/AssetContextMenu'
|
||||
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as assetTreeNode from '#/utilities/assetTreeNode'
|
||||
import * as dateTime from '#/utilities/dateTime'
|
||||
import * as download from '#/utilities/download'
|
||||
import * as drag from '#/utilities/drag'
|
||||
import * as errorModule from '#/utilities/error'
|
||||
import * as indent from '#/utilities/indent'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
import * as set from '#/utilities/set'
|
||||
import Visibility, * as visibilityModule from '#/utilities/visibility'
|
||||
|
||||
import * as assetsTable from './assetsTable'
|
||||
import type * as tableRow from './tableRow'
|
||||
import StatelessSpinner, * as statelessSpinner from './statelessSpinner'
|
||||
import AssetContextMenu from './assetContextMenu'
|
||||
import TableRow from './tableRow'
|
||||
import StatelessSpinner, * as statelessSpinner from '#/components/StatelessSpinner'
|
||||
import type * as tableRow from '#/components/TableRow'
|
||||
import TableRow from '#/components/TableRow'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -35,6 +35,9 @@ import TableRow from './tableRow'
|
||||
* to make a directory row expand. */
|
||||
const DRAG_EXPAND_DELAY_MS = 500
|
||||
|
||||
/** Placeholder row for directories that are empty. */
|
||||
const EMPTY_DIRECTORY_PLACEHOLDER = <span className="px-2 opacity-75">This folder is empty.</span>
|
||||
|
||||
// ================
|
||||
// === AssetRow ===
|
||||
// ================
|
||||
@ -50,42 +53,21 @@ export interface AssetRowProps
|
||||
|
||||
/** A row containing an {@link backendModule.AnyAsset}. */
|
||||
export default function AssetRow(props: AssetRowProps) {
|
||||
const {
|
||||
keyProp: key,
|
||||
item: rawItem,
|
||||
initialRowState,
|
||||
hidden,
|
||||
selected,
|
||||
isSoleSelectedItem,
|
||||
setSelected,
|
||||
allowContextMenu,
|
||||
onContextMenu,
|
||||
state,
|
||||
columns,
|
||||
} = props
|
||||
const {
|
||||
visibilities,
|
||||
assetEvents,
|
||||
dispatchAssetEvent,
|
||||
dispatchAssetListEvent,
|
||||
setAssetSettingsPanelProps,
|
||||
doToggleDirectoryExpansion,
|
||||
doCopy,
|
||||
doCut,
|
||||
doPaste,
|
||||
} = state
|
||||
const { organization } = authProvider.useNonPartialUserSession()
|
||||
const { keyProp: key, item: rawItem, initialRowState, hidden, selected } = props
|
||||
const { isSoleSelectedItem, setSelected, allowContextMenu, onContextMenu, state } = props
|
||||
const { columns } = props
|
||||
const { visibilities, assetEvents, dispatchAssetEvent, dispatchAssetListEvent } = state
|
||||
const { setAssetSettingsPanelProps, doToggleDirectoryExpansion, doCopy, doCut, doPaste } = state
|
||||
|
||||
const { organization, user } = authProvider.useNonPartialUserSession()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const [isDraggedOver, setIsDraggedOver] = React.useState(false)
|
||||
const [item, setItem] = React.useState(rawItem)
|
||||
const dragOverTimeoutHandle = React.useRef<number | null>(null)
|
||||
const asset = item.item
|
||||
const [insertionVisibility, setInsertionVisibility] = React.useState(
|
||||
visibilityModule.Visibility.visible
|
||||
)
|
||||
const [insertionVisibility, setInsertionVisibility] = React.useState(Visibility.visible)
|
||||
const [rowState, setRowState] = React.useState<assetsTable.AssetRowState>(() =>
|
||||
object.merge(initialRowState, { setVisibility: setInsertionVisibility })
|
||||
)
|
||||
@ -102,7 +84,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
const setAsset = assetTreeNode.useSetAsset(asset, setItem)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selected && insertionVisibility !== visibilityModule.Visibility.visible) {
|
||||
if (selected && insertionVisibility !== Visibility.visible) {
|
||||
setSelected(false)
|
||||
}
|
||||
}, [selected, insertionVisibility, /* should never change */ setSelected])
|
||||
@ -134,7 +116,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
toastAndLog(`Could not copy '${asset.title}'`, error)
|
||||
// Delete the new component representing the asset that failed to insert.
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
}
|
||||
@ -161,7 +143,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
const nonNullNewParentId = newParentId ?? rootDirectoryId
|
||||
try {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.move,
|
||||
type: AssetListEventType.move,
|
||||
newParentKey: nonNullNewParentKey,
|
||||
newParentId: nonNullNewParentId,
|
||||
key: item.key,
|
||||
@ -188,7 +170,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
)
|
||||
// Move the asset back to its original position.
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.move,
|
||||
type: AssetListEventType.move,
|
||||
newParentKey: item.directoryKey,
|
||||
newParentId: item.directoryId,
|
||||
key: item.key,
|
||||
@ -215,10 +197,10 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}, [item, isSoleSelectedItem, /* should never change */ setAssetSettingsPanelProps])
|
||||
|
||||
const doDelete = React.useCallback(async () => {
|
||||
setInsertionVisibility(visibilityModule.Visibility.hidden)
|
||||
setInsertionVisibility(Visibility.hidden)
|
||||
if (asset.type === backendModule.AssetType.directory) {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.closeFolder,
|
||||
type: AssetListEventType.closeFolder,
|
||||
id: asset.id,
|
||||
// This is SAFE, as this asset is already known to be a directory.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@ -227,7 +209,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
try {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.willDelete,
|
||||
type: AssetListEventType.willDelete,
|
||||
key: item.key,
|
||||
})
|
||||
if (
|
||||
@ -248,11 +230,11 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
await backend.deleteAsset(asset.id, asset.title)
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
} catch (error) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.visible)
|
||||
setInsertionVisibility(Visibility.visible)
|
||||
toastAndLog(
|
||||
errorModule.tryGetMessage(error)?.slice(0, -1) ??
|
||||
`Could not delete ${backendModule.ASSET_TYPE_NAME[asset.type]}`
|
||||
@ -268,15 +250,15 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
|
||||
const doRestore = React.useCallback(async () => {
|
||||
// Visually, the asset is deleted from the Trash view.
|
||||
setInsertionVisibility(visibilityModule.Visibility.hidden)
|
||||
setInsertionVisibility(Visibility.hidden)
|
||||
try {
|
||||
await backend.undoDeleteAsset(asset.id, asset.title)
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
} catch (error) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.visible)
|
||||
setInsertionVisibility(Visibility.visible)
|
||||
toastAndLog(`Unable to restore ${backendModule.ASSET_TYPE_NAME[asset.type]}`, error)
|
||||
}
|
||||
}, [
|
||||
@ -290,53 +272,53 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
hooks.useEventHandler(assetEvents, async event => {
|
||||
switch (event.type) {
|
||||
// These events are handled in the specific `NameColumn` files.
|
||||
case assetEventModule.AssetEventType.newProject:
|
||||
case assetEventModule.AssetEventType.newFolder:
|
||||
case assetEventModule.AssetEventType.uploadFiles:
|
||||
case assetEventModule.AssetEventType.newDataConnector:
|
||||
case assetEventModule.AssetEventType.openProject:
|
||||
case assetEventModule.AssetEventType.closeProject:
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects: {
|
||||
case AssetEventType.newProject:
|
||||
case AssetEventType.newFolder:
|
||||
case AssetEventType.uploadFiles:
|
||||
case AssetEventType.newDataConnector:
|
||||
case AssetEventType.openProject:
|
||||
case AssetEventType.closeProject:
|
||||
case AssetEventType.cancelOpeningAllProjects: {
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.copy: {
|
||||
case AssetEventType.copy: {
|
||||
if (event.ids.has(item.key)) {
|
||||
await doCopyOnBackend(event.newParentId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.cut: {
|
||||
case AssetEventType.cut: {
|
||||
if (event.ids.has(item.key)) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.faded)
|
||||
setInsertionVisibility(Visibility.faded)
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.cancelCut: {
|
||||
case AssetEventType.cancelCut: {
|
||||
if (event.ids.has(item.key)) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.visible)
|
||||
setInsertionVisibility(Visibility.visible)
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.move: {
|
||||
case AssetEventType.move: {
|
||||
if (event.ids.has(item.key)) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.visible)
|
||||
setInsertionVisibility(Visibility.visible)
|
||||
await doMove(event.newParentKey, event.newParentId)
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.delete: {
|
||||
case AssetEventType.delete: {
|
||||
if (event.ids.has(item.key)) {
|
||||
await doDelete()
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.restore: {
|
||||
case AssetEventType.restore: {
|
||||
if (event.ids.has(item.key)) {
|
||||
await doRestore()
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.download: {
|
||||
case AssetEventType.download: {
|
||||
if (event.ids.has(item.key)) {
|
||||
download.download(
|
||||
`./api/project-manager/projects/${asset.id}/enso-project`,
|
||||
@ -345,7 +327,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.downloadSelected: {
|
||||
case AssetEventType.downloadSelected: {
|
||||
if (selected) {
|
||||
download.download(
|
||||
`./api/project-manager/projects/${asset.id}/enso-project`,
|
||||
@ -354,10 +336,10 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.removeSelf: {
|
||||
case AssetEventType.removeSelf: {
|
||||
// This is not triggered from the asset list, so it uses `item.id` instead of `key`.
|
||||
if (event.id === asset.id && user != null) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.hidden)
|
||||
setInsertionVisibility(Visibility.hidden)
|
||||
try {
|
||||
await backend.createPermission({
|
||||
action: null,
|
||||
@ -365,17 +347,17 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
userSubjects: [user.id],
|
||||
})
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
} catch (error) {
|
||||
setInsertionVisibility(visibilityModule.Visibility.visible)
|
||||
setInsertionVisibility(Visibility.visible)
|
||||
toastAndLog(null, error)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.temporarilyAddLabels: {
|
||||
case AssetEventType.temporarilyAddLabels: {
|
||||
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY
|
||||
setRowState(oldRowState =>
|
||||
oldRowState.temporarilyAddedLabels === labels &&
|
||||
@ -388,7 +370,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
)
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.temporarilyRemoveLabels: {
|
||||
case AssetEventType.temporarilyRemoveLabels: {
|
||||
const labels = event.ids.has(item.key) ? event.labelNames : set.EMPTY
|
||||
setRowState(oldRowState =>
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY &&
|
||||
@ -401,7 +383,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
)
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.addLabels: {
|
||||
case AssetEventType.addLabels: {
|
||||
setRowState(oldRowState =>
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY
|
||||
? oldRowState
|
||||
@ -426,7 +408,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.removeLabels: {
|
||||
case AssetEventType.removeLabels: {
|
||||
setRowState(oldRowState =>
|
||||
oldRowState.temporarilyAddedLabels === set.EMPTY
|
||||
? oldRowState
|
||||
@ -449,7 +431,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.deleteLabel: {
|
||||
case AssetEventType.deleteLabel: {
|
||||
setAsset(oldAsset => {
|
||||
// The IIFE is required to prevent TypeScript from narrowing this value.
|
||||
let found = (() => false)()
|
||||
@ -502,9 +484,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
isDraggedOver ? 'selected' : ''
|
||||
}`}
|
||||
{...props}
|
||||
hidden={
|
||||
hidden || insertionVisibility === visibilityModule.Visibility.hidden
|
||||
}
|
||||
hidden={hidden || insertionVisibility === Visibility.hidden}
|
||||
onContextMenu={(innerProps, event) => {
|
||||
if (allowContextMenu) {
|
||||
event.preventDefault()
|
||||
@ -591,7 +571,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
true
|
||||
)
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.move,
|
||||
type: AssetEventType.move,
|
||||
newParentKey: directoryKey,
|
||||
newParentId: directoryId,
|
||||
ids: new Set(payload.map(dragItem => dragItem.key)),
|
||||
@ -599,9 +579,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{selected &&
|
||||
allowContextMenu &&
|
||||
insertionVisibility !== visibilityModule.Visibility.hidden && (
|
||||
{selected && allowContextMenu && insertionVisibility !== Visibility.hidden && (
|
||||
// This is a copy of the context menu, since the context menu registers keyboard
|
||||
// shortcut handlers. This is a bit of a hack, however it is preferable to duplicating
|
||||
// the entire context menu (once for the keyboard actions, once for the JSX).
|
||||
@ -654,7 +632,7 @@ export default function AssetRow(props: AssetRowProps) {
|
||||
)}`}
|
||||
>
|
||||
<img src={BlankIcon} />
|
||||
{assetsTable.EMPTY_DIRECTORY_PLACEHOLDER}
|
||||
{EMPTY_DIRECTORY_PLACEHOLDER}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
@ -1,12 +1,13 @@
|
||||
/** @file Modal for confirming delete of any type of asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as toastify from 'react-toastify'
|
||||
|
||||
import * as errorModule from '../../error'
|
||||
import * as loggerProvider from '../../providers/logger'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as loggerProvider from '#/providers/LoggerProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as errorModule from '#/utilities/error'
|
||||
|
||||
import Modal from './modal'
|
||||
import Modal from '#/components/Modal'
|
||||
|
||||
// ==========================
|
||||
// === ConfirmDeleteModal ===
|
@ -3,21 +3,21 @@ import * as React from 'react'
|
||||
|
||||
import ConnectorIcon from 'enso-assets/connector.svg'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as eventModule from '../event'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as indent from '../indent'
|
||||
import * as object from '../../object'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as visibility from '../visibility'
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import AssetListEventType from '#/events/AssetListEventType'
|
||||
import * as hooks from '#/hooks'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as assetTreeNode from '#/utilities/assetTreeNode'
|
||||
import * as eventModule from '#/utilities/event'
|
||||
import * as indent from '#/utilities/indent'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
import Visibility from '#/utilities/visibility'
|
||||
|
||||
import type * as column from '../column'
|
||||
import EditableSpan from './editableSpan'
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import EditableSpan from '#/components/EditableSpan'
|
||||
|
||||
// =====================
|
||||
// === ConnectorName ===
|
||||
@ -30,14 +30,8 @@ export interface ConnectorNameColumnProps extends column.AssetColumnProps {}
|
||||
* @throws {Error} when the asset is not a {@link backendModule.SecretAsset}.
|
||||
* This should never happen. */
|
||||
export default function ConnectorNameColumn(props: ConnectorNameColumnProps) {
|
||||
const {
|
||||
item,
|
||||
setItem,
|
||||
selected,
|
||||
state: { assetEvents, dispatchAssetListEvent },
|
||||
rowState,
|
||||
setRowState,
|
||||
} = props
|
||||
const { item, setItem, selected, state, rowState, setRowState } = props
|
||||
const { assetEvents, dispatchAssetListEvent } = state
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
@ -57,48 +51,48 @@ export default function ConnectorNameColumn(props: ConnectorNameColumnProps) {
|
||||
|
||||
hooks.useEventHandler(assetEvents, async event => {
|
||||
switch (event.type) {
|
||||
case assetEventModule.AssetEventType.newProject:
|
||||
case assetEventModule.AssetEventType.newFolder:
|
||||
case assetEventModule.AssetEventType.uploadFiles:
|
||||
case assetEventModule.AssetEventType.openProject:
|
||||
case assetEventModule.AssetEventType.closeProject:
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects:
|
||||
case assetEventModule.AssetEventType.copy:
|
||||
case assetEventModule.AssetEventType.cut:
|
||||
case assetEventModule.AssetEventType.cancelCut:
|
||||
case assetEventModule.AssetEventType.move:
|
||||
case assetEventModule.AssetEventType.delete:
|
||||
case assetEventModule.AssetEventType.restore:
|
||||
case assetEventModule.AssetEventType.download:
|
||||
case assetEventModule.AssetEventType.downloadSelected:
|
||||
case assetEventModule.AssetEventType.removeSelf:
|
||||
case assetEventModule.AssetEventType.temporarilyAddLabels:
|
||||
case assetEventModule.AssetEventType.temporarilyRemoveLabels:
|
||||
case assetEventModule.AssetEventType.addLabels:
|
||||
case assetEventModule.AssetEventType.removeLabels:
|
||||
case assetEventModule.AssetEventType.deleteLabel: {
|
||||
case AssetEventType.newProject:
|
||||
case AssetEventType.newFolder:
|
||||
case AssetEventType.uploadFiles:
|
||||
case AssetEventType.openProject:
|
||||
case AssetEventType.closeProject:
|
||||
case AssetEventType.cancelOpeningAllProjects:
|
||||
case AssetEventType.copy:
|
||||
case AssetEventType.cut:
|
||||
case AssetEventType.cancelCut:
|
||||
case AssetEventType.move:
|
||||
case AssetEventType.delete:
|
||||
case AssetEventType.restore:
|
||||
case AssetEventType.download:
|
||||
case AssetEventType.downloadSelected:
|
||||
case AssetEventType.removeSelf:
|
||||
case AssetEventType.temporarilyAddLabels:
|
||||
case AssetEventType.temporarilyRemoveLabels:
|
||||
case AssetEventType.addLabels:
|
||||
case AssetEventType.removeLabels:
|
||||
case AssetEventType.deleteLabel: {
|
||||
// Ignored. These events should all be unrelated to secrets.
|
||||
// `deleteMultiple`, `restoreMultiple`, `download`,
|
||||
// and `downloadSelected` are handled by `AssetRow`.
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.newDataConnector: {
|
||||
case AssetEventType.newDataConnector: {
|
||||
if (item.key === event.placeholderId) {
|
||||
if (backend.type !== backendModule.BackendType.remote) {
|
||||
toastAndLog('Data connectors cannot be created on the local backend')
|
||||
} else {
|
||||
rowState.setVisibility(visibility.Visibility.faded)
|
||||
rowState.setVisibility(Visibility.faded)
|
||||
try {
|
||||
const createdSecret = await backend.createSecret({
|
||||
parentDirectoryId: asset.parentId,
|
||||
secretName: asset.title,
|
||||
secretValue: event.value,
|
||||
})
|
||||
rowState.setVisibility(visibility.Visibility.visible)
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(object.merger({ id: createdSecret.id }))
|
||||
} catch (error) {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
toastAndLog('Error creating new data connector', error)
|
@ -4,22 +4,22 @@ import * as React from 'react'
|
||||
import FolderIcon from 'enso-assets/folder.svg'
|
||||
import TriangleDownIcon from 'enso-assets/triangle_down.svg'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import type * as column from '../column'
|
||||
import * as eventModule from '../event'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as indent from '../indent'
|
||||
import * as object from '../../object'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as visibility from '../visibility'
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import AssetListEventType from '#/events/AssetListEventType'
|
||||
import * as hooks from '#/hooks'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as assetTreeNode from '#/utilities/assetTreeNode'
|
||||
import * as eventModule from '#/utilities/event'
|
||||
import * as indent from '#/utilities/indent'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
import Visibility from '#/utilities/visibility'
|
||||
|
||||
import EditableSpan from './editableSpan'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import EditableSpan from '#/components/EditableSpan'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// =====================
|
||||
// === DirectoryName ===
|
||||
@ -32,21 +32,9 @@ export interface DirectoryNameColumnProps extends column.AssetColumnProps {}
|
||||
* @throws {Error} when the asset is not a {@link backendModule.DirectoryAsset}.
|
||||
* This should never happen. */
|
||||
export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
const {
|
||||
item,
|
||||
setItem,
|
||||
selected,
|
||||
setSelected,
|
||||
state: {
|
||||
numberOfSelectedItems,
|
||||
assetEvents,
|
||||
dispatchAssetListEvent,
|
||||
nodeMap,
|
||||
doToggleDirectoryExpansion,
|
||||
},
|
||||
rowState,
|
||||
setRowState,
|
||||
} = props
|
||||
const { item, setItem, selected, setSelected, state, rowState, setRowState } = props
|
||||
const { numberOfSelectedItems, assetEvents, dispatchAssetListEvent, nodeMap } = state
|
||||
const { doToggleDirectoryExpansion } = state
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
@ -71,47 +59,47 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
|
||||
|
||||
hooks.useEventHandler(assetEvents, async event => {
|
||||
switch (event.type) {
|
||||
case assetEventModule.AssetEventType.newProject:
|
||||
case assetEventModule.AssetEventType.uploadFiles:
|
||||
case assetEventModule.AssetEventType.newDataConnector:
|
||||
case assetEventModule.AssetEventType.openProject:
|
||||
case assetEventModule.AssetEventType.closeProject:
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects:
|
||||
case assetEventModule.AssetEventType.copy:
|
||||
case assetEventModule.AssetEventType.cut:
|
||||
case assetEventModule.AssetEventType.cancelCut:
|
||||
case assetEventModule.AssetEventType.move:
|
||||
case assetEventModule.AssetEventType.delete:
|
||||
case assetEventModule.AssetEventType.restore:
|
||||
case assetEventModule.AssetEventType.download:
|
||||
case assetEventModule.AssetEventType.downloadSelected:
|
||||
case assetEventModule.AssetEventType.removeSelf:
|
||||
case assetEventModule.AssetEventType.temporarilyAddLabels:
|
||||
case assetEventModule.AssetEventType.temporarilyRemoveLabels:
|
||||
case assetEventModule.AssetEventType.addLabels:
|
||||
case assetEventModule.AssetEventType.removeLabels:
|
||||
case assetEventModule.AssetEventType.deleteLabel: {
|
||||
case AssetEventType.newProject:
|
||||
case AssetEventType.uploadFiles:
|
||||
case AssetEventType.newDataConnector:
|
||||
case AssetEventType.openProject:
|
||||
case AssetEventType.closeProject:
|
||||
case AssetEventType.cancelOpeningAllProjects:
|
||||
case AssetEventType.copy:
|
||||
case AssetEventType.cut:
|
||||
case AssetEventType.cancelCut:
|
||||
case AssetEventType.move:
|
||||
case AssetEventType.delete:
|
||||
case AssetEventType.restore:
|
||||
case AssetEventType.download:
|
||||
case AssetEventType.downloadSelected:
|
||||
case AssetEventType.removeSelf:
|
||||
case AssetEventType.temporarilyAddLabels:
|
||||
case AssetEventType.temporarilyRemoveLabels:
|
||||
case AssetEventType.addLabels:
|
||||
case AssetEventType.removeLabels:
|
||||
case AssetEventType.deleteLabel: {
|
||||
// Ignored. These events should all be unrelated to directories.
|
||||
// `deleteMultiple`, `restoreMultiple`, `download`,
|
||||
// and `downloadSelected` are handled by `AssetRow`.
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.newFolder: {
|
||||
case AssetEventType.newFolder: {
|
||||
if (item.key === event.placeholderId) {
|
||||
if (backend.type !== backendModule.BackendType.remote) {
|
||||
toastAndLog('Cannot create folders on the local drive')
|
||||
} else {
|
||||
rowState.setVisibility(visibility.Visibility.faded)
|
||||
rowState.setVisibility(Visibility.faded)
|
||||
try {
|
||||
const createdDirectory = await backend.createDirectory({
|
||||
parentId: asset.parentId,
|
||||
title: asset.title,
|
||||
})
|
||||
rowState.setVisibility(visibility.Visibility.visible)
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(object.merge(asset, createdDirectory))
|
||||
} catch (error) {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
toastAndLog('Could not create new folder', error)
|
@ -1,23 +1,23 @@
|
||||
/** @file The icon and name of a {@link backendModule.FileAsset}. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as eventModule from '../event'
|
||||
import * as fileIcon from '../../fileIcon'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as indent from '../indent'
|
||||
import * as object from '../../object'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as visibility from '../visibility'
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import AssetListEventType from '#/events/AssetListEventType'
|
||||
import * as hooks from '#/hooks'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as assetTreeNode from '#/utilities/assetTreeNode'
|
||||
import * as eventModule from '#/utilities/event'
|
||||
import * as fileIcon from '#/utilities/fileIcon'
|
||||
import * as indent from '#/utilities/indent'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
import Visibility from '#/utilities/visibility'
|
||||
|
||||
import type * as column from '../column'
|
||||
import EditableSpan from './editableSpan'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import EditableSpan from '#/components/EditableSpan'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// ================
|
||||
// === FileName ===
|
||||
@ -30,14 +30,8 @@ export interface FileNameColumnProps extends column.AssetColumnProps {}
|
||||
* @throws {Error} when the asset is not a {@link backendModule.FileAsset}.
|
||||
* This should never happen. */
|
||||
export default function FileNameColumn(props: FileNameColumnProps) {
|
||||
const {
|
||||
item,
|
||||
setItem,
|
||||
selected,
|
||||
state: { assetEvents, dispatchAssetListEvent },
|
||||
rowState,
|
||||
setRowState,
|
||||
} = props
|
||||
const { item, setItem, selected, state, rowState, setRowState } = props
|
||||
const { assetEvents, dispatchAssetListEvent } = state
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
@ -57,35 +51,35 @@ export default function FileNameColumn(props: FileNameColumnProps) {
|
||||
|
||||
hooks.useEventHandler(assetEvents, async event => {
|
||||
switch (event.type) {
|
||||
case assetEventModule.AssetEventType.newProject:
|
||||
case assetEventModule.AssetEventType.newFolder:
|
||||
case assetEventModule.AssetEventType.newDataConnector:
|
||||
case assetEventModule.AssetEventType.openProject:
|
||||
case assetEventModule.AssetEventType.closeProject:
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects:
|
||||
case assetEventModule.AssetEventType.copy:
|
||||
case assetEventModule.AssetEventType.cut:
|
||||
case assetEventModule.AssetEventType.cancelCut:
|
||||
case assetEventModule.AssetEventType.move:
|
||||
case assetEventModule.AssetEventType.delete:
|
||||
case assetEventModule.AssetEventType.restore:
|
||||
case assetEventModule.AssetEventType.download:
|
||||
case assetEventModule.AssetEventType.downloadSelected:
|
||||
case assetEventModule.AssetEventType.removeSelf:
|
||||
case assetEventModule.AssetEventType.temporarilyAddLabels:
|
||||
case assetEventModule.AssetEventType.temporarilyRemoveLabels:
|
||||
case assetEventModule.AssetEventType.addLabels:
|
||||
case assetEventModule.AssetEventType.removeLabels:
|
||||
case assetEventModule.AssetEventType.deleteLabel: {
|
||||
case AssetEventType.newProject:
|
||||
case AssetEventType.newFolder:
|
||||
case AssetEventType.newDataConnector:
|
||||
case AssetEventType.openProject:
|
||||
case AssetEventType.closeProject:
|
||||
case AssetEventType.cancelOpeningAllProjects:
|
||||
case AssetEventType.copy:
|
||||
case AssetEventType.cut:
|
||||
case AssetEventType.cancelCut:
|
||||
case AssetEventType.move:
|
||||
case AssetEventType.delete:
|
||||
case AssetEventType.restore:
|
||||
case AssetEventType.download:
|
||||
case AssetEventType.downloadSelected:
|
||||
case AssetEventType.removeSelf:
|
||||
case AssetEventType.temporarilyAddLabels:
|
||||
case AssetEventType.temporarilyRemoveLabels:
|
||||
case AssetEventType.addLabels:
|
||||
case AssetEventType.removeLabels:
|
||||
case AssetEventType.deleteLabel: {
|
||||
// Ignored. These events should all be unrelated to projects.
|
||||
// `deleteMultiple`, `restoreMultiple`, `download`, and `downloadSelected`
|
||||
// are handled by `AssetRow`.
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.uploadFiles: {
|
||||
case AssetEventType.uploadFiles: {
|
||||
const file = event.files.get(item.key)
|
||||
if (file != null) {
|
||||
rowState.setVisibility(visibility.Visibility.faded)
|
||||
rowState.setVisibility(Visibility.faded)
|
||||
try {
|
||||
const createdFile = await backend.uploadFile(
|
||||
{
|
||||
@ -95,11 +89,11 @@ export default function FileNameColumn(props: FileNameColumnProps) {
|
||||
},
|
||||
file
|
||||
)
|
||||
rowState.setVisibility(visibility.Visibility.visible)
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(object.merge(asset, { id: createdFile.id }))
|
||||
} catch (error) {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
toastAndLog('Could not upload file', error)
|
@ -1,21 +1,7 @@
|
||||
/** @file An label that can be applied to an asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backend from '../backend'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The default color for labels (Light blue). */
|
||||
export const DEFAULT_LABEL_COLOR: backend.LChColor = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
lightness: 100,
|
||||
chroma: 0,
|
||||
hue: 0,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
alpha: 70,
|
||||
}
|
||||
import * as backend from '#/services/backend'
|
||||
|
||||
// =============
|
||||
// === Label ===
|
@ -0,0 +1,16 @@
|
||||
/** @file Constants related to labels. */
|
||||
import type * as backend from '#/services/backend'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
// The default color for labels (Light blue).
|
||||
export const DEFAULT_LABEL_COLOR: backend.LChColor = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
lightness: 100,
|
||||
chroma: 0,
|
||||
hue: 0,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
alpha: 70,
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/** @file Colored border around icons and text indicating permissions. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as permissionsModule from '../permissions'
|
||||
import * as permissionsModule from '#/utilities/permissions'
|
||||
|
||||
// =================
|
||||
// === Component ===
|
@ -1,12 +1,12 @@
|
||||
/** @file A selector for all possible permissions. */
|
||||
import * as React from 'react'
|
||||
|
||||
import type * as backend from '../backend'
|
||||
import type * as permissions from '../permissions'
|
||||
import * as permissionsModule from '../permissions'
|
||||
import type * as backend from '#/services/backend'
|
||||
import type * as permissions from '#/utilities/permissions'
|
||||
import * as permissionsModule from '#/utilities/permissions'
|
||||
|
||||
import Modal from './modal'
|
||||
import PermissionTypeSelector from './permissionTypeSelector'
|
||||
import PermissionTypeSelector from '#/components/dashboard/PermissionTypeSelector'
|
||||
import Modal from '#/components/Modal'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -46,18 +46,8 @@ export interface PermissionSelectorProps {
|
||||
|
||||
/** A horizontal selector for all possible permissions. */
|
||||
export default function PermissionSelector(props: PermissionSelectorProps) {
|
||||
const {
|
||||
showDelete = false,
|
||||
disabled = false,
|
||||
typeSelectorYOffsetPx,
|
||||
error,
|
||||
selfPermission,
|
||||
action: actionRaw,
|
||||
assetType,
|
||||
className,
|
||||
onChange,
|
||||
doDelete,
|
||||
} = props
|
||||
const { showDelete = false, disabled = false, typeSelectorYOffsetPx, error } = props
|
||||
const { selfPermission, action: actionRaw, assetType, className, onChange, doDelete } = props
|
||||
const [action, setActionRaw] = React.useState(actionRaw)
|
||||
const [TheChild, setTheChild] = React.useState<(() => JSX.Element) | null>()
|
||||
const permission = permissionsModule.FROM_PERMISSION_ACTION[action]
|
@ -1,8 +1,8 @@
|
||||
/** @file A selector for all possible permission types. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backend from '../backend'
|
||||
import * as permissions from '../permissions'
|
||||
import * as backend from '#/services/backend'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
@ -1,25 +1,27 @@
|
||||
/** @file An interactive button indicating the status of a project. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as toast from 'react-toastify'
|
||||
|
||||
import ArrowUpIcon from 'enso-assets/arrow_up.svg'
|
||||
import PlayIcon from 'enso-assets/play.svg'
|
||||
import StopIcon from 'enso-assets/stop.svg'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as errorModule from '../../error'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as localStorageModule from '../localStorage'
|
||||
import * as localStorageProvider from '../../providers/localStorage'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as object from '../../object'
|
||||
import * as remoteBackend from '../remoteBackend'
|
||||
import type * as assetEvent from '#/events/assetEvent'
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import * as hooks from '#/hooks'
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as localStorageProvider from '#/providers/LocalStorageProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as remoteBackend from '#/services/remoteBackend'
|
||||
import * as errorModule from '#/utilities/error'
|
||||
import * as localStorageModule from '#/utilities/localStorage'
|
||||
import * as object from '#/utilities/object'
|
||||
|
||||
import Spinner, * as spinner from './spinner'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import Spinner, * as spinner from '#/components/Spinner'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
@ -65,7 +67,7 @@ export interface ProjectIconProps {
|
||||
keyProp: string
|
||||
item: backendModule.ProjectAsset
|
||||
setItem: React.Dispatch<React.SetStateAction<backendModule.ProjectAsset>>
|
||||
assetEvents: assetEventModule.AssetEvent[]
|
||||
assetEvents: assetEvent.AssetEvent[]
|
||||
/** Called when the project is opened via the {@link ProjectIcon}. */
|
||||
doOpenManually: (projectId: backendModule.ProjectId) => void
|
||||
onClose: () => void
|
||||
@ -234,29 +236,29 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
|
||||
hooks.useEventHandler(assetEvents, event => {
|
||||
switch (event.type) {
|
||||
case assetEventModule.AssetEventType.newFolder:
|
||||
case assetEventModule.AssetEventType.uploadFiles:
|
||||
case assetEventModule.AssetEventType.newDataConnector:
|
||||
case assetEventModule.AssetEventType.copy:
|
||||
case assetEventModule.AssetEventType.cut:
|
||||
case assetEventModule.AssetEventType.cancelCut:
|
||||
case assetEventModule.AssetEventType.move:
|
||||
case assetEventModule.AssetEventType.delete:
|
||||
case assetEventModule.AssetEventType.restore:
|
||||
case assetEventModule.AssetEventType.download:
|
||||
case assetEventModule.AssetEventType.downloadSelected:
|
||||
case assetEventModule.AssetEventType.removeSelf:
|
||||
case assetEventModule.AssetEventType.temporarilyAddLabels:
|
||||
case assetEventModule.AssetEventType.temporarilyRemoveLabels:
|
||||
case assetEventModule.AssetEventType.addLabels:
|
||||
case assetEventModule.AssetEventType.removeLabels:
|
||||
case assetEventModule.AssetEventType.deleteLabel: {
|
||||
case AssetEventType.newFolder:
|
||||
case AssetEventType.uploadFiles:
|
||||
case AssetEventType.newDataConnector:
|
||||
case AssetEventType.copy:
|
||||
case AssetEventType.cut:
|
||||
case AssetEventType.cancelCut:
|
||||
case AssetEventType.move:
|
||||
case AssetEventType.delete:
|
||||
case AssetEventType.restore:
|
||||
case AssetEventType.download:
|
||||
case AssetEventType.downloadSelected:
|
||||
case AssetEventType.removeSelf:
|
||||
case AssetEventType.temporarilyAddLabels:
|
||||
case AssetEventType.temporarilyRemoveLabels:
|
||||
case AssetEventType.addLabels:
|
||||
case AssetEventType.removeLabels:
|
||||
case AssetEventType.deleteLabel: {
|
||||
// Ignored. Any missing project-related events should be handled by
|
||||
// `ProjectNameColumn`. `deleteMultiple`, `restoreMultiple`, `download`,
|
||||
// and `downloadSelected` are handled by `AssetRow`.
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.openProject: {
|
||||
case AssetEventType.openProject: {
|
||||
if (event.id !== item.id) {
|
||||
if (!event.runInBackground && !isRunningInBackground) {
|
||||
setShouldOpenWhenReady(false)
|
||||
@ -272,14 +274,14 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.closeProject: {
|
||||
case AssetEventType.closeProject: {
|
||||
if (event.id === item.id) {
|
||||
setShouldOpenWhenReady(false)
|
||||
void closeProject(false)
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects: {
|
||||
case AssetEventType.cancelOpeningAllProjects: {
|
||||
if (!isRunningInBackground) {
|
||||
setShouldOpenWhenReady(false)
|
||||
onSpinnerStateChange?.(null)
|
||||
@ -292,7 +294,7 @@ export default function ProjectIcon(props: ProjectIconProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.newProject: {
|
||||
case AssetEventType.newProject: {
|
||||
if (event.placeholderId === key) {
|
||||
setOnSpinnerStateChange(() => event.onSpinnerStateChange)
|
||||
} else if (event.onSpinnerStateChange === onSpinnerStateChange) {
|
@ -3,27 +3,27 @@ import * as React from 'react'
|
||||
|
||||
import NetworkIcon from 'enso-assets/network.svg'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as errorModule from '../../error'
|
||||
import * as eventModule from '../event'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as indent from '../indent'
|
||||
import * as object from '../../object'
|
||||
import * as permissions from '../permissions'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
import * as validation from '../validation'
|
||||
import * as visibility from '../visibility'
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import AssetListEventType from '#/events/AssetListEventType'
|
||||
import * as hooks from '#/hooks'
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as assetTreeNode from '#/utilities/assetTreeNode'
|
||||
import * as errorModule from '#/utilities/error'
|
||||
import * as eventModule from '#/utilities/event'
|
||||
import * as indent from '#/utilities/indent'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
import * as validation from '#/utilities/validation'
|
||||
import Visibility from '#/utilities/visibility'
|
||||
|
||||
import type * as column from '../column'
|
||||
import EditableSpan from './editableSpan'
|
||||
import ProjectIcon from './projectIcon'
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import ProjectIcon from '#/components/dashboard/ProjectIcon'
|
||||
import EditableSpan from '#/components/EditableSpan'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// ===================
|
||||
// === ProjectName ===
|
||||
@ -36,23 +36,9 @@ export interface ProjectNameColumnProps extends column.AssetColumnProps {}
|
||||
* @throws {Error} when the asset is not a {@link backendModule.ProjectAsset}.
|
||||
* This should never happen. */
|
||||
export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
const {
|
||||
item,
|
||||
setItem,
|
||||
selected,
|
||||
rowState,
|
||||
setRowState,
|
||||
state: {
|
||||
numberOfSelectedItems,
|
||||
assetEvents,
|
||||
dispatchAssetEvent,
|
||||
dispatchAssetListEvent,
|
||||
nodeMap,
|
||||
doOpenManually,
|
||||
doOpenIde,
|
||||
doCloseIde,
|
||||
},
|
||||
} = props
|
||||
const { item, setItem, selected, rowState, setRowState, state } = props
|
||||
const { numberOfSelectedItems, assetEvents, dispatchAssetEvent, dispatchAssetListEvent } = state
|
||||
const { nodeMap, doOpenManually, doOpenIde, doCloseIde } = state
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const { organization } = authProvider.useNonPartialUserSession()
|
||||
@ -69,7 +55,9 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
// This is a workaround for a temporary bad state in the backend causing the `projectState` key
|
||||
// to be absent.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const projectState = asset.projectState ?? { type: backendModule.ProjectState.closed }
|
||||
const projectState = asset.projectState ?? {
|
||||
type: backendModule.ProjectState.closed,
|
||||
}
|
||||
const isRunning = backendModule.DOES_PROJECT_STATE_INDICATE_VM_EXISTS[projectState.type]
|
||||
const canExecute =
|
||||
backend.type === backendModule.BackendType.local ||
|
||||
@ -100,43 +88,43 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
|
||||
hooks.useEventHandler(assetEvents, async event => {
|
||||
switch (event.type) {
|
||||
case assetEventModule.AssetEventType.newFolder:
|
||||
case assetEventModule.AssetEventType.newDataConnector:
|
||||
case assetEventModule.AssetEventType.openProject:
|
||||
case assetEventModule.AssetEventType.closeProject:
|
||||
case assetEventModule.AssetEventType.cancelOpeningAllProjects:
|
||||
case assetEventModule.AssetEventType.copy:
|
||||
case assetEventModule.AssetEventType.cut:
|
||||
case assetEventModule.AssetEventType.cancelCut:
|
||||
case assetEventModule.AssetEventType.move:
|
||||
case assetEventModule.AssetEventType.delete:
|
||||
case assetEventModule.AssetEventType.restore:
|
||||
case assetEventModule.AssetEventType.download:
|
||||
case assetEventModule.AssetEventType.downloadSelected:
|
||||
case assetEventModule.AssetEventType.removeSelf:
|
||||
case assetEventModule.AssetEventType.temporarilyAddLabels:
|
||||
case assetEventModule.AssetEventType.temporarilyRemoveLabels:
|
||||
case assetEventModule.AssetEventType.addLabels:
|
||||
case assetEventModule.AssetEventType.removeLabels:
|
||||
case assetEventModule.AssetEventType.deleteLabel: {
|
||||
case AssetEventType.newFolder:
|
||||
case AssetEventType.newDataConnector:
|
||||
case AssetEventType.openProject:
|
||||
case AssetEventType.closeProject:
|
||||
case AssetEventType.cancelOpeningAllProjects:
|
||||
case AssetEventType.copy:
|
||||
case AssetEventType.cut:
|
||||
case AssetEventType.cancelCut:
|
||||
case AssetEventType.move:
|
||||
case AssetEventType.delete:
|
||||
case AssetEventType.restore:
|
||||
case AssetEventType.download:
|
||||
case AssetEventType.downloadSelected:
|
||||
case AssetEventType.removeSelf:
|
||||
case AssetEventType.temporarilyAddLabels:
|
||||
case AssetEventType.temporarilyRemoveLabels:
|
||||
case AssetEventType.addLabels:
|
||||
case AssetEventType.removeLabels:
|
||||
case AssetEventType.deleteLabel: {
|
||||
// Ignored. Any missing project-related events should be handled by `ProjectIcon`.
|
||||
// `deleteMultiple`, `restoreMultiple`, `download`, and `downloadSelected`
|
||||
// are handled by `AssetRow`.
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.newProject: {
|
||||
case AssetEventType.newProject: {
|
||||
// This should only run before this project gets replaced with the actual project
|
||||
// by this event handler. In both cases `key` will match, so using `key` here
|
||||
// is a mistake.
|
||||
if (asset.id === event.placeholderId) {
|
||||
rowState.setVisibility(visibility.Visibility.faded)
|
||||
rowState.setVisibility(Visibility.faded)
|
||||
try {
|
||||
const createdProject = await backend.createProject({
|
||||
parentDirectoryId: asset.parentId,
|
||||
projectName: asset.title,
|
||||
projectTemplateName: event.templateId,
|
||||
})
|
||||
rowState.setVisibility(visibility.Visibility.visible)
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(
|
||||
object.merge(asset, {
|
||||
id: createdProject.projectId,
|
||||
@ -146,14 +134,14 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
})
|
||||
)
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
type: AssetEventType.openProject,
|
||||
id: createdProject.projectId,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
} catch (error) {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
toastAndLog('Error creating new project', error)
|
||||
@ -161,10 +149,10 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case assetEventModule.AssetEventType.uploadFiles: {
|
||||
case AssetEventType.uploadFiles: {
|
||||
const file = event.files.get(item.key)
|
||||
if (file != null) {
|
||||
rowState.setVisibility(visibility.Visibility.faded)
|
||||
rowState.setVisibility(Visibility.faded)
|
||||
try {
|
||||
if (backend.type === backendModule.BackendType.local) {
|
||||
let id: string
|
||||
@ -190,7 +178,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
backendModule.ProjectId(id),
|
||||
null
|
||||
)
|
||||
rowState.setVisibility(visibility.Visibility.visible)
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(
|
||||
object.merge(asset, {
|
||||
title: listedProject.packageName,
|
||||
@ -213,7 +201,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
if (project == null) {
|
||||
throw new Error('The uploaded file was not a project.')
|
||||
} else {
|
||||
rowState.setVisibility(visibility.Visibility.visible)
|
||||
rowState.setVisibility(Visibility.visible)
|
||||
setAsset(
|
||||
object.merge(asset, {
|
||||
title,
|
||||
@ -226,7 +214,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
}
|
||||
} catch (error) {
|
||||
dispatchAssetListEvent({
|
||||
type: assetListEventModule.AssetListEventType.delete,
|
||||
type: AssetListEventType.delete,
|
||||
key: item.key,
|
||||
})
|
||||
toastAndLog('Could not upload project', error)
|
||||
@ -253,14 +241,14 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
|
||||
} else if (shortcuts.matchesMouseAction(shortcutsModule.MouseAction.open, event)) {
|
||||
// It is a double click; open the project.
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
type: AssetEventType.openProject,
|
||||
id: asset.id,
|
||||
shouldAutomaticallySwitchPage: true,
|
||||
runInBackground: false,
|
||||
})
|
||||
} else if (shortcuts.matchesMouseAction(shortcutsModule.MouseAction.run, event)) {
|
||||
dispatchAssetEvent({
|
||||
type: assetEventModule.AssetEventType.openProject,
|
||||
type: AssetEventType.openProject,
|
||||
id: asset.id,
|
||||
shouldAutomaticallySwitchPage: false,
|
||||
runInBackground: true,
|
@ -1,7 +1,7 @@
|
||||
/** @file A component that renders the modal instance from the modal React Context. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
|
||||
// ================
|
||||
// === TheModal ===
|
@ -1,12 +1,12 @@
|
||||
/** @file A user and their permissions for a specific asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as object from '../../object'
|
||||
import * as hooks from '#/hooks'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as backendModule from '#/services/backend'
|
||||
import * as object from '#/utilities/object'
|
||||
|
||||
import PermissionSelector from './permissionSelector'
|
||||
import PermissionSelector from '#/components/dashboard/PermissionSelector'
|
||||
|
||||
/** Props for a {@link UserPermissions}. */
|
||||
export interface UserPermissionsProps {
|
||||
@ -20,14 +20,9 @@ export interface UserPermissionsProps {
|
||||
|
||||
/** A user and their permissions for a specific asset. */
|
||||
export default function UserPermissions(props: UserPermissionsProps) {
|
||||
const {
|
||||
asset,
|
||||
self,
|
||||
isOnlyOwner,
|
||||
userPermission: initialUserPermission,
|
||||
setUserPermission: outerSetUserPermission,
|
||||
doDelete,
|
||||
} = props
|
||||
const { asset, self, isOnlyOwner, doDelete } = props
|
||||
const { userPermission: initialUserPermission, setUserPermission: outerSetUserPermission } =
|
||||
props
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const [userPermissions, setUserPermissions] = React.useState(initialUserPermission)
|
@ -0,0 +1,40 @@
|
||||
/** @file Column types and column display modes. */
|
||||
|
||||
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
|
||||
import type * as backendModule from '#/services/backend'
|
||||
import type * as assetTreeNode from '#/utilities/assetTreeNode'
|
||||
|
||||
import AssetNameColumn from '#/components/dashboard/AssetNameColumn'
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import DocsColumn from '#/components/dashboard/column/DocsColumn'
|
||||
import LabelsColumn from '#/components/dashboard/column/LabelsColumn'
|
||||
import LastModifiedColumn from '#/components/dashboard/column/LastModifiedColumn'
|
||||
import PlaceholderColumn from '#/components/dashboard/column/PlaceholderColumn'
|
||||
import SharedWithColumn from '#/components/dashboard/column/SharedWithColumn'
|
||||
import type * as tableColumn from '#/components/TableColumn'
|
||||
|
||||
// ==========================
|
||||
// === LastModifiedColumn ===
|
||||
// ==========================
|
||||
|
||||
/** {@link tableColumn.TableColumnProps} for an unknown variant of {@link backendModule.Asset}. */
|
||||
export type AssetColumnProps = tableColumn.TableColumnProps<
|
||||
assetTreeNode.AssetTreeNode,
|
||||
assetsTable.AssetsTableState,
|
||||
assetsTable.AssetRowState,
|
||||
backendModule.AssetId
|
||||
>
|
||||
|
||||
/** React components for every column except for the name column. */
|
||||
// This is not a React component even though it contains JSX.
|
||||
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unused-vars
|
||||
export const COLUMN_RENDERER: Record<columnUtils.Column, (props: AssetColumnProps) => JSX.Element> =
|
||||
{
|
||||
[columnUtils.Column.name]: AssetNameColumn,
|
||||
[columnUtils.Column.modified]: LastModifiedColumn,
|
||||
[columnUtils.Column.sharedWith]: SharedWithColumn,
|
||||
[columnUtils.Column.labels]: LabelsColumn,
|
||||
[columnUtils.Column.accessedByProjects]: PlaceholderColumn,
|
||||
[columnUtils.Column.accessedData]: PlaceholderColumn,
|
||||
[columnUtils.Column.docs]: DocsColumn,
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
/** @file A column listing the users with which this asset is shared. */
|
||||
import * as React from 'react'
|
||||
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
|
||||
/** A column listing the users with which this asset is shared. */
|
||||
export default function DocsColumn(props: column.AssetColumnProps) {
|
||||
const { item } = props
|
||||
return <div className="flex items-center gap-1">{item.item.description}</div>
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/** @file A column listing the labels on this asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import Plus2Icon from 'enso-assets/plus2.svg'
|
||||
|
||||
import * as hooks from '#/hooks'
|
||||
import Category from '#/layouts/dashboard/CategorySwitcher/Category'
|
||||
import ManageLabelsModal from '#/layouts/dashboard/ManageLabelsModal'
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import type * as backendModule from '#/services/backend'
|
||||
import * as assetQuery from '#/utilities/assetQuery'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
import * as shortcuts from '#/utilities/shortcuts'
|
||||
import * as uniqueString from '#/utilities/uniqueString'
|
||||
|
||||
import ContextMenu from '#/components/ContextMenu'
|
||||
import ContextMenus from '#/components/ContextMenus'
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import Label from '#/components/dashboard/Label'
|
||||
import * as labelUtils from '#/components/dashboard/Label/labelUtils'
|
||||
import MenuEntry from '#/components/MenuEntry'
|
||||
|
||||
// ====================
|
||||
// === LabelsColumn ===
|
||||
// ====================
|
||||
|
||||
/** A column listing the labels on this asset. */
|
||||
export default function LabelsColumn(props: column.AssetColumnProps) {
|
||||
const { item, setItem, state, rowState } = props
|
||||
const { category, labels, setQuery, deletedLabelNames, doCreateLabel } = state
|
||||
const { temporarilyAddedLabels, temporarilyRemovedLabels } = rowState
|
||||
const asset = item.item
|
||||
const session = authProvider.useNonPartialUserSession()
|
||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const toastAndLog = hooks.useToastAndLog()
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
const self = asset.permissions?.find(
|
||||
permission => permission.user.user_email === session.organization?.email
|
||||
)
|
||||
const managesThisAsset =
|
||||
category !== Category.trash &&
|
||||
(self?.permission === permissions.PermissionAction.own ||
|
||||
self?.permission === permissions.PermissionAction.admin)
|
||||
const setAsset = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<backendModule.AnyAsset>) => {
|
||||
setItem(oldItem =>
|
||||
object.merge(oldItem, {
|
||||
item:
|
||||
typeof valueOrUpdater !== 'function'
|
||||
? valueOrUpdater
|
||||
: valueOrUpdater(oldItem.item),
|
||||
})
|
||||
)
|
||||
},
|
||||
[/* should never change */ setItem]
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
{(asset.labels ?? [])
|
||||
.filter(label => !deletedLabelNames.has(label))
|
||||
.map(label => (
|
||||
<Label
|
||||
key={label}
|
||||
title="Right click to remove label."
|
||||
color={labels.get(label)?.color ?? labelUtils.DEFAULT_LABEL_COLOR}
|
||||
active={!temporarilyRemovedLabels.has(label)}
|
||||
disabled={temporarilyRemovedLabels.has(label)}
|
||||
negated={temporarilyRemovedLabels.has(label)}
|
||||
className={
|
||||
temporarilyRemovedLabels.has(label)
|
||||
? 'relative before:absolute before:rounded-full before:border-2 before:border-delete before:inset-0 before:w-full before:h-full'
|
||||
: ''
|
||||
}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const doDelete = () => {
|
||||
unsetModal()
|
||||
setAsset(oldAsset => {
|
||||
const newLabels =
|
||||
oldAsset.labels?.filter(oldLabel => oldLabel !== label) ??
|
||||
[]
|
||||
void backend
|
||||
.associateTag(asset.id, newLabels, asset.title)
|
||||
.catch(error => {
|
||||
toastAndLog(null, error)
|
||||
setAsset(oldAsset2 =>
|
||||
oldAsset2.labels?.some(
|
||||
oldLabel => oldLabel === label
|
||||
) === true
|
||||
? oldAsset2
|
||||
: object.merge(oldAsset2, {
|
||||
labels: [
|
||||
...(oldAsset2.labels ?? []),
|
||||
label,
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
return object.merge(oldAsset, { labels: newLabels })
|
||||
})
|
||||
}
|
||||
setModal(
|
||||
<ContextMenus key={`label-${label}`} event={event}>
|
||||
<ContextMenu>
|
||||
<MenuEntry
|
||||
action={shortcuts.KeyboardAction.delete}
|
||||
doAction={doDelete}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</ContextMenus>
|
||||
)
|
||||
}}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setQuery(oldQuery =>
|
||||
assetQuery.toggleLabel(oldQuery, label, event.shiftKey)
|
||||
)
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
))}
|
||||
{...[...temporarilyAddedLabels]
|
||||
.filter(label => asset.labels?.includes(label) !== true)
|
||||
.map(label => (
|
||||
<Label
|
||||
disabled
|
||||
key={label}
|
||||
color={labels.get(label)?.color ?? labelUtils.DEFAULT_LABEL_COLOR}
|
||||
className="pointer-events-none"
|
||||
onClick={() => {}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
))}
|
||||
{managesThisAsset && (
|
||||
<button
|
||||
className={`h-4 w-4 ${isHovered ? '' : 'invisible pointer-events-none'}`}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
setModal(
|
||||
<ManageLabelsModal
|
||||
key={uniqueString.uniqueString()}
|
||||
item={asset}
|
||||
setItem={setAsset}
|
||||
allLabels={labels}
|
||||
doCreateLabel={doCreateLabel}
|
||||
eventTarget={event.currentTarget}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img className="w-4.5 h-4.5" src={Plus2Icon} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
/** @file A column displaying the time at which the asset was last modified. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as dateTime from '#/utilities/dateTime'
|
||||
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
|
||||
/** A column displaying the time at which the asset was last modified. */
|
||||
export default function LastModifiedColumn(props: column.AssetColumnProps) {
|
||||
return <>{dateTime.formatDateTime(new Date(props.item.item.modifiedAt))}</>
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
/** @file A placeholder component for columns which do not yet have corresponding data to display. */
|
||||
import * as React from 'react'
|
||||
|
||||
// =========================
|
||||
// === PlaceholderColumn ===
|
||||
// =========================
|
||||
|
||||
/** A placeholder component for columns which do not yet have corresponding data to display. */
|
||||
export default function PlaceholderColumn() {
|
||||
return <></>
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
/** @file A column listing the users with which this asset is shared. */
|
||||
import * as React from 'react'
|
||||
|
||||
import Plus2Icon from 'enso-assets/plus2.svg'
|
||||
|
||||
import AssetEventType from '#/events/AssetEventType'
|
||||
import Category from '#/layouts/dashboard/CategorySwitcher/Category'
|
||||
import ManagePermissionsModal from '#/layouts/dashboard/ManagePermissionsModal'
|
||||
import * as authProvider from '#/providers/AuthProvider'
|
||||
import * as modalProvider from '#/providers/ModalProvider'
|
||||
import type * as backendModule from '#/services/backend'
|
||||
import * as object from '#/utilities/object'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
import * as uniqueString from '#/utilities/uniqueString'
|
||||
|
||||
import type * as column from '#/components/dashboard/column'
|
||||
import PermissionDisplay from '#/components/dashboard/PermissionDisplay'
|
||||
|
||||
// ========================
|
||||
// === SharedWithColumn ===
|
||||
// ========================
|
||||
|
||||
/** The type of the `state` prop of a {@link SharedWithColumn}. */
|
||||
interface SharedWithColumnStateProp {
|
||||
category: column.AssetColumnProps['state']['category']
|
||||
dispatchAssetEvent: column.AssetColumnProps['state']['dispatchAssetEvent']
|
||||
}
|
||||
|
||||
/** Props for a {@link SharedWithColumn}. */
|
||||
interface SharedWithColumnPropsInternal extends Pick<column.AssetColumnProps, 'item' | 'setItem'> {
|
||||
state: SharedWithColumnStateProp
|
||||
}
|
||||
|
||||
/** A column listing the users with which this asset is shared. */
|
||||
export default function SharedWithColumn(props: SharedWithColumnPropsInternal) {
|
||||
const { item, setItem, state } = props
|
||||
const { category, dispatchAssetEvent } = state
|
||||
const asset = item.item
|
||||
const session = authProvider.useNonPartialUserSession()
|
||||
const { setModal } = modalProvider.useSetModal()
|
||||
const self = asset.permissions?.find(
|
||||
permission => permission.user.user_email === session.organization?.email
|
||||
)
|
||||
const managesThisAsset =
|
||||
category !== Category.trash &&
|
||||
(self?.permission === permissions.PermissionAction.own ||
|
||||
self?.permission === permissions.PermissionAction.admin)
|
||||
const setAsset = React.useCallback(
|
||||
(valueOrUpdater: React.SetStateAction<backendModule.AnyAsset>) => {
|
||||
setItem(oldItem =>
|
||||
object.merge(oldItem, {
|
||||
item:
|
||||
typeof valueOrUpdater !== 'function'
|
||||
? valueOrUpdater
|
||||
: valueOrUpdater(oldItem.item),
|
||||
})
|
||||
)
|
||||
},
|
||||
[/* should never change */ setItem]
|
||||
)
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{(asset.permissions ?? []).map(user => (
|
||||
<PermissionDisplay key={user.user.pk} action={user.permission}>
|
||||
{user.user.user_name}
|
||||
</PermissionDisplay>
|
||||
))}
|
||||
{managesThisAsset && (
|
||||
<button
|
||||
className="h-4 w-4 invisible pointer-events-none group-hover:visible group-hover:pointer-events-auto"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
setModal(
|
||||
<ManagePermissionsModal
|
||||
key={uniqueString.uniqueString()}
|
||||
item={asset}
|
||||
setItem={setAsset}
|
||||
self={self}
|
||||
eventTarget={event.currentTarget}
|
||||
doRemoveSelf={() => {
|
||||
dispatchAssetEvent({
|
||||
type: AssetEventType.removeSelf,
|
||||
id: asset.id,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img className="w-4.5 h-4.5" src={Plus2Icon} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/** @file Types and constants related to `Column`s. */
|
||||
import AccessedByProjectsIcon from 'enso-assets/accessed_by_projects.svg'
|
||||
import AccessedDataIcon from 'enso-assets/accessed_data.svg'
|
||||
import DocsIcon from 'enso-assets/docs.svg'
|
||||
import SortAscendingIcon from 'enso-assets/sort_ascending.svg'
|
||||
import SortDescendingIcon from 'enso-assets/sort_descending.svg'
|
||||
import TagIcon from 'enso-assets/tag.svg'
|
||||
|
||||
import * as backend from '#/services/backend'
|
||||
import * as sorting from '#/utilities/sorting'
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
// =============
|
||||
|
||||
/** Determines which columns are visible. */
|
||||
export enum ColumnDisplayMode {
|
||||
/** Show only columns which are ready for release. */
|
||||
release = 'release',
|
||||
/** Show all columns. */
|
||||
all = 'all',
|
||||
/** Show only name and metadata. */
|
||||
compact = 'compact',
|
||||
/** Show only columns relevant to documentation editors. */
|
||||
docs = 'docs',
|
||||
/** Show only name, metadata, and configuration options. */
|
||||
settings = 'settings',
|
||||
}
|
||||
|
||||
/** Column type. */
|
||||
export enum Column {
|
||||
name = 'name',
|
||||
modified = 'modified',
|
||||
sharedWith = 'sharedWith',
|
||||
labels = 'labels',
|
||||
accessedByProjects = 'accessedByProjects',
|
||||
accessedData = 'accessedData',
|
||||
docs = 'docs',
|
||||
}
|
||||
|
||||
/** Columns that can be toggled between visible and hidden. */
|
||||
export type ExtraColumn = (typeof EXTRA_COLUMNS)[number]
|
||||
|
||||
/** Columns that can be used as a sort column. */
|
||||
export type SortableColumn = Column.modified | Column.name
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The list of extra columns, in order. */
|
||||
// This MUST be `as const`, to generate the `ExtraColumn` type above.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export const EXTRA_COLUMNS = [
|
||||
Column.labels,
|
||||
Column.accessedByProjects,
|
||||
Column.accessedData,
|
||||
Column.docs,
|
||||
] as const
|
||||
|
||||
export const EXTRA_COLUMN_IMAGES: Record<ExtraColumn, string> = {
|
||||
[Column.labels]: TagIcon,
|
||||
[Column.accessedByProjects]: AccessedByProjectsIcon,
|
||||
[Column.accessedData]: AccessedDataIcon,
|
||||
[Column.docs]: DocsIcon,
|
||||
}
|
||||
|
||||
/** English names for every column except for the name column. */
|
||||
export const COLUMN_NAME: Record<Column, string> = {
|
||||
[Column.name]: 'Name',
|
||||
[Column.modified]: 'Modified',
|
||||
[Column.sharedWith]: 'Shared with',
|
||||
[Column.labels]: 'Labels',
|
||||
[Column.accessedByProjects]: 'Accessed by projects',
|
||||
[Column.accessedData]: 'Accessed data',
|
||||
[Column.docs]: 'Docs',
|
||||
} as const
|
||||
|
||||
const COLUMN_CSS_CLASSES =
|
||||
'text-left bg-clip-padding border-transparent border-l-2 border-r-2 last:border-r-0'
|
||||
const NORMAL_COLUMN_CSS_CLASSES = `px-2 last:rounded-r-full last:w-full ${COLUMN_CSS_CLASSES}`
|
||||
|
||||
/** CSS classes for every column. */
|
||||
export const COLUMN_CSS_CLASS: Record<Column, string> = {
|
||||
[Column.name]: `rounded-rows-skip-level min-w-61.25 p-0 border-l-0 ${COLUMN_CSS_CLASSES}`,
|
||||
[Column.modified]: `min-w-33.25 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.sharedWith]: `min-w-40 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.labels]: `min-w-80 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.accessedByProjects]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.accessedData]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
[Column.docs]: `min-w-96 ${NORMAL_COLUMN_CSS_CLASSES}`,
|
||||
} as const
|
||||
|
||||
// =====================
|
||||
// === getColumnList ===
|
||||
// =====================
|
||||
|
||||
/** Return the full list of columns given the relevant current state. */
|
||||
export function getColumnList(backendType: backend.BackendType, extraColumns: Set<ExtraColumn>) {
|
||||
switch (backendType) {
|
||||
case backend.BackendType.local: {
|
||||
return [Column.name, Column.modified]
|
||||
}
|
||||
case backend.BackendType.remote: {
|
||||
return [
|
||||
Column.name,
|
||||
Column.modified,
|
||||
Column.sharedWith,
|
||||
...EXTRA_COLUMNS.filter(column => extraColumns.has(column)),
|
||||
]
|
||||
}
|
||||
}
|
||||
} // =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
/** The corresponding icon URL for each {@link sorting.SortDirection}. */
|
||||
|
||||
export const SORT_ICON: Record<sorting.SortDirection, string> = {
|
||||
[sorting.SortDirection.ascending]: SortAscendingIcon,
|
||||
[sorting.SortDirection.descending]: SortDescendingIcon,
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/** @file A lookup containing a component for the corresponding heading for each column type. */
|
||||
|
||||
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import AccessedByProjectsColumnHeading from '#/components/dashboard/columnHeading/AccessedByProjectsColumnHeading'
|
||||
import AccessedDataColumnHeading from '#/components/dashboard/columnHeading/AccessedDataColumnHeading'
|
||||
import DocsColumnHeading from '#/components/dashboard/columnHeading/DocsColumnHeading'
|
||||
import LabelsColumnHeading from '#/components/dashboard/columnHeading/LabelsColumnHeading'
|
||||
import ModifiedColumnHeading from '#/components/dashboard/columnHeading/ModifiedColumnHeading'
|
||||
import NameColumnHeading from '#/components/dashboard/columnHeading/NameColumnHeading'
|
||||
import SharedWithColumnHeading from '#/components/dashboard/columnHeading/SharedWithColumnHeading'
|
||||
import type * as tableColumn from '#/components/TableColumn'
|
||||
|
||||
export const COLUMN_HEADING: Record<
|
||||
columnUtils.Column,
|
||||
(props: tableColumn.TableColumnHeadingProps<assetsTable.AssetsTableState>) => JSX.Element
|
||||
> = {
|
||||
[columnUtils.Column.name]: NameColumnHeading,
|
||||
[columnUtils.Column.modified]: ModifiedColumnHeading,
|
||||
[columnUtils.Column.sharedWith]: SharedWithColumnHeading,
|
||||
[columnUtils.Column.labels]: LabelsColumnHeading,
|
||||
[columnUtils.Column.accessedByProjects]: AccessedByProjectsColumnHeading,
|
||||
[columnUtils.Column.accessedData]: AccessedDataColumnHeading,
|
||||
[columnUtils.Column.docs]: DocsColumnHeading,
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @file A heading for the "Accessed by projects" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import AccessedByProjectsIcon from 'enso-assets/accessed_by_projects.svg'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
/** A heading for the "Accessed by projects" column. */
|
||||
export default function AccessedByProjectsColumnHeading(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={AccessedByProjectsIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.accessedByProjects]}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @file A heading for the "Accessed data" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import AccessedDataIcon from 'enso-assets/accessed_data.svg'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
/** A heading for the "Accessed data" column. */
|
||||
export default function AccessedDataColumnHeading(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={AccessedDataIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.accessedData]}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @file A heading for the "Docs" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import DocsIcon from 'enso-assets/docs.svg'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
/** A heading for the "Docs" column. */
|
||||
export default function DocsColumnHeading(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={DocsIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.docs]}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @file A heading for the "Labels" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import TagIcon from 'enso-assets/tag.svg'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
/** A heading for the "Labels" column. */
|
||||
export default function LabelsColumnHeading(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={TagIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.labels]}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/** @file A heading for the "Modified" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import SortAscendingIcon from 'enso-assets/sort_ascending.svg'
|
||||
import TimeIcon from 'enso-assets/time.svg'
|
||||
|
||||
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
|
||||
import * as sorting from '#/utilities/sorting'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
import type * as tableColumn from '#/components/TableColumn'
|
||||
|
||||
/** A heading for the "Modified" column. */
|
||||
export default function ModifiedColumnHeading(
|
||||
props: tableColumn.TableColumnHeadingProps<assetsTable.AssetsTableState>
|
||||
): JSX.Element {
|
||||
const { state } = props
|
||||
const { sortColumn, setSortColumn, sortDirection, setSortDirection } = state
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
const isSortActive = sortColumn === columnUtils.Column.modified && sortDirection != null
|
||||
return (
|
||||
<div
|
||||
className="flex items-center cursor-pointer gap-2"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (sortColumn === columnUtils.Column.modified) {
|
||||
setSortDirection(sorting.NEXT_SORT_DIRECTION[sortDirection ?? 'null'])
|
||||
} else {
|
||||
setSortColumn(columnUtils.Column.modified)
|
||||
setSortDirection(sorting.SortDirection.ascending)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SvgMask src={TimeIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.modified]}
|
||||
</span>
|
||||
<img
|
||||
src={isSortActive ? columnUtils.SORT_ICON[sortDirection] : SortAscendingIcon}
|
||||
className={isSortActive ? '' : isHovered ? 'opacity-50' : 'opacity-0'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/** @file A heading for the "Name" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import SortAscendingIcon from 'enso-assets/sort_ascending.svg'
|
||||
|
||||
import type * as assetsTable from '#/layouts/dashboard/AssetsTable'
|
||||
import * as sorting from '#/utilities/sorting'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import type * as tableColumn from '#/components/TableColumn'
|
||||
|
||||
/** A heading for the "Name" column. */
|
||||
export default function NameColumnHeading(
|
||||
props: tableColumn.TableColumnHeadingProps<assetsTable.AssetsTableState>
|
||||
): JSX.Element {
|
||||
const { state } = props
|
||||
const { sortColumn, setSortColumn, sortDirection, setSortDirection } = state
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
const isSortActive = sortColumn === columnUtils.Column.name && sortDirection != null
|
||||
return (
|
||||
<div
|
||||
className="flex items-center cursor-pointer gap-2 pt-1 pb-1.5"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (sortColumn === columnUtils.Column.name) {
|
||||
setSortDirection(sorting.NEXT_SORT_DIRECTION[sortDirection ?? 'null'])
|
||||
} else {
|
||||
setSortColumn(columnUtils.Column.name)
|
||||
setSortDirection(sorting.SortDirection.ascending)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.name]}
|
||||
</span>
|
||||
<img
|
||||
src={isSortActive ? columnUtils.SORT_ICON[sortDirection] : SortAscendingIcon}
|
||||
className={isSortActive ? '' : isHovered ? 'opacity-50' : 'opacity-0'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/** @file A heading for the "Shared with" column. */
|
||||
import * as React from 'react'
|
||||
|
||||
import PeopleIcon from 'enso-assets/people.svg'
|
||||
|
||||
import * as columnUtils from '#/components/dashboard/column/columnUtils'
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
/** A heading for the "Shared with" column. */
|
||||
export default function SharedWithColumnHeading(): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<SvgMask src={PeopleIcon} className="h-4 w-4" />
|
||||
<span className="leading-144.5 h-6 py-0.5">
|
||||
{columnUtils.COLUMN_NAME[columnUtils.Column.sharedWith]}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -6,12 +6,12 @@ import CtrlKeyIcon from 'enso-assets/ctrl_key.svg'
|
||||
import OptionKeyIcon from 'enso-assets/option_key.svg'
|
||||
import ShiftKeyIcon from 'enso-assets/shift_key.svg'
|
||||
import WindowsKeyIcon from 'enso-assets/windows_key.svg'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
import * as shortcutsModule from '../shortcuts'
|
||||
import * as shortcutsProvider from '../../providers/shortcuts'
|
||||
|
||||
import SvgMask from '../../authentication/components/svgMask'
|
||||
import * as shortcutsProvider from '#/providers/ShortcutsProvider'
|
||||
import * as shortcutsModule from '#/utilities/shortcuts'
|
||||
|
||||
import SvgMask from '#/components/SvgMask'
|
||||
|
||||
// ========================
|
||||
// === KeyboardShortcut ===
|
@ -1,14 +1,13 @@
|
||||
/** @file Entry point into the cloud dashboard. */
|
||||
import * as authentication from 'enso-authentication'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import './tailwind.css'
|
||||
import '#/tailwind.css'
|
||||
|
||||
import * as authentication from '#/index'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** Path to the SSE endpoint over which esbuild sends events. */
|
||||
const ESBUILD_PATH = './esbuild'
|
||||
/** SSE event indicating a build has finished. */
|
34
app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts
Normal file
34
app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/** @file Possible types of asset state change. */
|
||||
|
||||
// ======================
|
||||
// === AssetEventType ===
|
||||
// ======================
|
||||
|
||||
/** Possible types of asset state change. */
|
||||
enum AssetEventType {
|
||||
newProject = 'new-project',
|
||||
newFolder = 'new-folder',
|
||||
uploadFiles = 'upload-files',
|
||||
newDataConnector = 'new-data-connector',
|
||||
openProject = 'open-project',
|
||||
closeProject = 'close-project',
|
||||
cancelOpeningAllProjects = 'cancel-opening-all-projects',
|
||||
copy = 'copy',
|
||||
cut = 'cut',
|
||||
cancelCut = 'cancel-cut',
|
||||
move = 'move',
|
||||
delete = 'delete',
|
||||
restore = 'restore',
|
||||
download = 'download',
|
||||
downloadSelected = 'download-selected',
|
||||
removeSelf = 'remove-self',
|
||||
temporarilyAddLabels = 'temporarily-add-labels',
|
||||
temporarilyRemoveLabels = 'temporarily-remove-labels',
|
||||
addLabels = 'add-labels',
|
||||
removeLabels = 'remove-labels',
|
||||
deleteLabel = 'delete-label',
|
||||
}
|
||||
|
||||
// This is REQUIRED, as `export default enum` is invalid syntax.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export default AssetEventType
|
@ -0,0 +1,19 @@
|
||||
/** @file Possible types of changes to the file list. */
|
||||
|
||||
/** Possible types of changes to the file list. */
|
||||
enum AssetListEventType {
|
||||
newFolder = 'new-folder',
|
||||
newProject = 'new-project',
|
||||
uploadFiles = 'upload-files',
|
||||
newDataConnector = 'new-data-connector',
|
||||
closeFolder = 'close-folder',
|
||||
copy = 'copy',
|
||||
move = 'move',
|
||||
willDelete = 'will-delete',
|
||||
delete = 'delete',
|
||||
removeSelf = 'remove-self',
|
||||
}
|
||||
|
||||
// This is REQUIRED, as `export default enum` is invalid syntax.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export default AssetListEventType
|
@ -1,11 +1,12 @@
|
||||
/** @file Events related to changes in asset state. */
|
||||
import type * as backendModule from '../backend'
|
||||
import type AssetEventType from '#/events/AssetEventType'
|
||||
import type * as backendModule from '#/services/backend'
|
||||
|
||||
import type * as spinner from '../components/spinner'
|
||||
import type * as spinner from '#/components/Spinner'
|
||||
|
||||
// This is required, to whitelist this event.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
declare module '../../hooks' {
|
||||
declare module '#/hooks/eventHooks' {
|
||||
/** A map containing all known event types. */
|
||||
export interface KnownEventsMap {
|
||||
assetEvent: AssetEvent
|
||||
@ -16,31 +17,6 @@ declare module '../../hooks' {
|
||||
// === AssetEvent ===
|
||||
// ==================
|
||||
|
||||
/** Possible types of asset state change. */
|
||||
export enum AssetEventType {
|
||||
newProject = 'new-project',
|
||||
newFolder = 'new-folder',
|
||||
uploadFiles = 'upload-files',
|
||||
newDataConnector = 'new-data-connector',
|
||||
openProject = 'open-project',
|
||||
closeProject = 'close-project',
|
||||
cancelOpeningAllProjects = 'cancel-opening-all-projects',
|
||||
copy = 'copy',
|
||||
cut = 'cut',
|
||||
cancelCut = 'cancel-cut',
|
||||
move = 'move',
|
||||
delete = 'delete',
|
||||
restore = 'restore',
|
||||
download = 'download',
|
||||
downloadSelected = 'download-selected',
|
||||
removeSelf = 'remove-self',
|
||||
temporarilyAddLabels = 'temporarily-add-labels',
|
||||
temporarilyRemoveLabels = 'temporarily-remove-labels',
|
||||
addLabels = 'add-labels',
|
||||
removeLabels = 'remove-labels',
|
||||
deleteLabel = 'delete-label',
|
||||
}
|
||||
|
||||
/** Properties common to all asset state change events. */
|
||||
interface AssetBaseEvent<Type extends AssetEventType> {
|
||||
type: Type
|
@ -1,30 +1,21 @@
|
||||
/** @file Events related to changes in the asset list. */
|
||||
import type * as backend from '../backend'
|
||||
import type AssetListEventType from '#/events/AssetListEventType'
|
||||
import type * as backend from '#/services/backend'
|
||||
|
||||
import type * as spinner from '../components/spinner'
|
||||
import type * as spinner from '#/components/Spinner'
|
||||
|
||||
// This is required, to whitelist this event.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
declare module '../../hooks' {
|
||||
declare module '#/hooks/eventHooks' {
|
||||
/** A map containing all known event types. */
|
||||
export interface KnownEventsMap {
|
||||
assetListEvent: AssetListEvent
|
||||
}
|
||||
}
|
||||
|
||||
/** Possible changes to the file list. */
|
||||
export enum AssetListEventType {
|
||||
newFolder = 'new-folder',
|
||||
newProject = 'new-project',
|
||||
uploadFiles = 'upload-files',
|
||||
newDataConnector = 'new-data-connector',
|
||||
closeFolder = 'close-folder',
|
||||
copy = 'copy',
|
||||
move = 'move',
|
||||
willDelete = 'will-delete',
|
||||
delete = 'delete',
|
||||
removeSelf = 'remove-self',
|
||||
}
|
||||
// ======================
|
||||
// === AssetListEvent ===
|
||||
// ======================
|
||||
|
||||
/** Properties common to all asset list events. */
|
||||
interface AssetListBaseEvent<Type extends AssetListEventType> {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user