mirror of
https://github.com/enso-org/enso.git
synced 2025-01-03 15:32:00 +03:00
Enable React and strict-boolean-expressions
lints (#7023)
* QoL improvements * Enable react lints and `strict-boolean-expressions` * Address review * Minor bugfixes --------- Co-authored-by: Paweł Buchowski <pawel.buchowski@enso.org>
This commit is contained in:
parent
d11f09c192
commit
c276bc035c
@ -10,6 +10,8 @@ import * as url from 'node:url'
|
|||||||
import eslintJs from '@eslint/js'
|
import eslintJs from '@eslint/js'
|
||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
import jsdoc from 'eslint-plugin-jsdoc'
|
import jsdoc from 'eslint-plugin-jsdoc'
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import tsEslint from '@typescript-eslint/eslint-plugin'
|
import tsEslint from '@typescript-eslint/eslint-plugin'
|
||||||
import tsEslintParser from '@typescript-eslint/parser'
|
import tsEslintParser from '@typescript-eslint/parser'
|
||||||
/* eslint-enable no-restricted-syntax */
|
/* eslint-enable no-restricted-syntax */
|
||||||
@ -241,6 +243,8 @@ export default [
|
|||||||
plugins: {
|
plugins: {
|
||||||
jsdoc: jsdoc,
|
jsdoc: jsdoc,
|
||||||
'@typescript-eslint': tsEslint,
|
'@typescript-eslint': tsEslint,
|
||||||
|
react: react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
},
|
},
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tsEslintParser,
|
parser: tsEslintParser,
|
||||||
@ -259,6 +263,7 @@ export default [
|
|||||||
...tsEslint.configs.recommended?.rules,
|
...tsEslint.configs.recommended?.rules,
|
||||||
...tsEslint.configs['recommended-requiring-type-checking']?.rules,
|
...tsEslint.configs['recommended-requiring-type-checking']?.rules,
|
||||||
...tsEslint.configs.strict?.rules,
|
...tsEslint.configs.strict?.rules,
|
||||||
|
...react.configs.recommended.rules,
|
||||||
eqeqeq: ['error', 'always', { null: 'never' }],
|
eqeqeq: ['error', 'always', { null: 'never' }],
|
||||||
'jsdoc/require-jsdoc': [
|
'jsdoc/require-jsdoc': [
|
||||||
'error',
|
'error',
|
||||||
@ -292,6 +297,10 @@ export default [
|
|||||||
'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES],
|
'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES],
|
||||||
'prefer-arrow-callback': 'error',
|
'prefer-arrow-callback': 'error',
|
||||||
'prefer-const': 'error',
|
'prefer-const': 'error',
|
||||||
|
// Not relevant because TypeScript checks types.
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
// Prefer `interface` over `type`.
|
// Prefer `interface` over `type`.
|
||||||
'@typescript-eslint/consistent-type-definitions': 'error',
|
'@typescript-eslint/consistent-type-definitions': 'error',
|
||||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'no-type-imports' }],
|
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'no-type-imports' }],
|
||||||
@ -355,7 +364,7 @@ export default [
|
|||||||
],
|
],
|
||||||
'@typescript-eslint/restrict-template-expressions': 'error',
|
'@typescript-eslint/restrict-template-expressions': 'error',
|
||||||
'@typescript-eslint/sort-type-constituents': 'error',
|
'@typescript-eslint/sort-type-constituents': 'error',
|
||||||
// '@typescript-eslint/strict-boolean-expressions': 'error',
|
'@typescript-eslint/strict-boolean-expressions': 'error',
|
||||||
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
||||||
'default-param-last': 'off',
|
'default-param-last': 'off',
|
||||||
'@typescript-eslint/default-param-last': 'error',
|
'@typescript-eslint/default-param-last': 'error',
|
||||||
|
@ -111,7 +111,7 @@ interface AmplifyError extends Error {
|
|||||||
|
|
||||||
/** Hint to TypeScript if we can safely cast an `unknown` error to an {@link AmplifyError}. */
|
/** Hint to TypeScript if we can safely cast an `unknown` error to an {@link AmplifyError}. */
|
||||||
function isAmplifyError(error: unknown): error is AmplifyError {
|
function isAmplifyError(error: unknown): error is AmplifyError {
|
||||||
if (error && typeof error === 'object') {
|
if (error != null && typeof error === 'object') {
|
||||||
return 'code' in error && 'message' in error && 'name' in error
|
return 'code' in error && 'message' in error && 'name' in error
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -141,7 +141,7 @@ interface AuthError {
|
|||||||
|
|
||||||
/** Hint to TypeScript if we can safely cast an `unknown` error to an `AuthError`. */
|
/** Hint to TypeScript if we can safely cast an `unknown` error to an `AuthError`. */
|
||||||
function isAuthError(error: unknown): error is AuthError {
|
function isAuthError(error: unknown): error is AuthError {
|
||||||
if (error && typeof error === 'object') {
|
if (error != null && typeof error === 'object') {
|
||||||
return 'name' in error && 'log' in error
|
return 'name' in error && 'log' in error
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -523,7 +523,7 @@ async function signInWithGoogle(customState: string | null) {
|
|||||||
const provider = amplify.CognitoHostedUIIdentityProvider.Google
|
const provider = amplify.CognitoHostedUIIdentityProvider.Google
|
||||||
const options = {
|
const options = {
|
||||||
provider,
|
provider,
|
||||||
...(customState ? { customState } : {}),
|
...(customState != null ? { customState } : {}),
|
||||||
}
|
}
|
||||||
await amplify.Auth.federatedSignIn(options)
|
await amplify.Auth.federatedSignIn(options)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Registration confirmation page for when a user clicks the confirmation link set to their
|
/** @file Registration confirmation page for when a user clicks the confirmation link set to their
|
||||||
* email address. */
|
* email address. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
@ -31,8 +31,10 @@ function ConfirmRegistration() {
|
|||||||
|
|
||||||
const { verificationCode, email } = parseUrlSearchParams(location.search)
|
const { verificationCode, email } = parseUrlSearchParams(location.search)
|
||||||
|
|
||||||
react.useEffect(() => {
|
// No dependencies means this runs on every render, however this component should immediately
|
||||||
if (!email || !verificationCode) {
|
// navigate away so it shouldn't exist for more than a few renders.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (email == null || verificationCode == null) {
|
||||||
navigate(app.LOGIN_PATH)
|
navigate(app.LOGIN_PATH)
|
||||||
} else {
|
} else {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@ -48,7 +50,7 @@ function ConfirmRegistration() {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
}, [])
|
})
|
||||||
|
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
/** @file Styled wrapper around FontAwesome icons. */
|
/** @file Styled wrapper around FontAwesome icons. */
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as fontawesome from '@fortawesome/react-fontawesome'
|
import * as fontawesome from '@fortawesome/react-fontawesome'
|
||||||
import * as fontawesomeIcons from '@fortawesome/free-brands-svg-icons'
|
import * as fontawesomeIcons from '@fortawesome/free-brands-svg-icons'
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Container responsible for rendering and interactions in first half of forgot password
|
/** @file Container responsible for rendering and interactions in first half of forgot password
|
||||||
* flow. */
|
* flow. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
|
|
||||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||||
@ -22,7 +22,7 @@ import SvgIcon from './svgIcon'
|
|||||||
function ForgotPassword() {
|
function ForgotPassword() {
|
||||||
const { forgotPassword } = auth.useAuth()
|
const { forgotPassword } = auth.useAuth()
|
||||||
|
|
||||||
const [email, setEmail] = react.useState('')
|
const [email, setEmail] = React.useState('')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
/** @file Styled input element. */
|
/** @file Styled input element. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// =============
|
// =============
|
||||||
// === Input ===
|
// === Input ===
|
||||||
// =============
|
// =============
|
||||||
|
|
||||||
/** Props for an {@link Input}. */
|
/** Props for an {@link Input}. */
|
||||||
export interface InputProps extends react.InputHTMLAttributes<HTMLInputElement> {
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
value: string
|
value: string
|
||||||
setValue: (value: string) => void
|
setValue: (value: string) => void
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Login component responsible for rendering and interactions in sign in flow. */
|
/** @file Login component responsible for rendering and interactions in sign in flow. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
|
|
||||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||||
@ -36,8 +36,8 @@ function Login() {
|
|||||||
|
|
||||||
const initialEmail = parseUrlSearchParams(search)
|
const initialEmail = parseUrlSearchParams(search)
|
||||||
|
|
||||||
const [email, setEmail] = react.useState(initialEmail ?? '')
|
const [email, setEmail] = React.useState(initialEmail ?? '')
|
||||||
const [password, setPassword] = react.useState('')
|
const [password, setPassword] = React.useState('')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Registration container responsible for rendering and interactions in sign up flow. */
|
/** @file Registration container responsible for rendering and interactions in sign up flow. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
@ -32,9 +32,9 @@ const REGISTRATION_QUERY_PARAMS = {
|
|||||||
function Registration() {
|
function Registration() {
|
||||||
const auth = authModule.useAuth()
|
const auth = authModule.useAuth()
|
||||||
const location = router.useLocation()
|
const location = router.useLocation()
|
||||||
const [email, setEmail] = react.useState('')
|
const [email, setEmail] = React.useState('')
|
||||||
const [password, setPassword] = react.useState('')
|
const [password, setPassword] = React.useState('')
|
||||||
const [confirmPassword, setConfirmPassword] = react.useState('')
|
const [confirmPassword, setConfirmPassword] = React.useState('')
|
||||||
|
|
||||||
const { organizationId } = parseUrlSearchParams(location.search)
|
const { organizationId } = parseUrlSearchParams(location.search)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Container responsible for rendering and interactions in second half of forgot password
|
/** @file Container responsible for rendering and interactions in second half of forgot password
|
||||||
* flow. */
|
* flow. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
@ -37,10 +37,10 @@ function ResetPassword() {
|
|||||||
|
|
||||||
const { verificationCode: initialCode, email: initialEmail } = parseUrlSearchParams(search)
|
const { verificationCode: initialCode, email: initialEmail } = parseUrlSearchParams(search)
|
||||||
|
|
||||||
const [email, setEmail] = react.useState(initialEmail ?? '')
|
const [email, setEmail] = React.useState(initialEmail ?? '')
|
||||||
const [code, setCode] = react.useState(initialCode ?? '')
|
const [code, setCode] = React.useState(initialCode ?? '')
|
||||||
const [newPassword, setNewPassword] = react.useState('')
|
const [newPassword, setNewPassword] = React.useState('')
|
||||||
const [newPasswordConfirm, setNewPasswordConfirm] = react.useState('')
|
const [newPasswordConfirm, setNewPasswordConfirm] = React.useState('')
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (newPassword !== newPasswordConfirm) {
|
if (newPassword !== newPasswordConfirm) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Container responsible for rendering and interactions in setting username flow, after
|
/** @file Container responsible for rendering and interactions in setting username flow, after
|
||||||
* registration. */
|
* registration. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||||
import AtIcon from 'enso-assets/at.svg'
|
import AtIcon from 'enso-assets/at.svg'
|
||||||
@ -22,7 +22,7 @@ function SetUsername() {
|
|||||||
const { email } = auth.usePartialUserSession()
|
const { email } = auth.usePartialUserSession()
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
|
|
||||||
const [username, setUsername] = react.useState('')
|
const [username, setUsername] = React.useState('')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
* Provides an `AuthProvider` component that wraps the entire application, and a `useAuth` hook that
|
* Provides an `AuthProvider` component that wraps the entire application, and a `useAuth` hook that
|
||||||
* can be used from any React component to access the currently logged-in user's session data. The
|
* can be used from any React component to access the currently logged-in user's session data. The
|
||||||
* hook also provides methods for registering a user, logging in, logging out, etc. */
|
* hook also provides methods for registering a user, logging in, logging out, etc. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ interface AuthContextType {
|
|||||||
* So changing the cast would provide no safety guarantees, and would require us to introduce null
|
* So changing the cast would provide no safety guarantees, and would require us to introduce null
|
||||||
* checks everywhere we use the context. */
|
* checks everywhere we use the context. */
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const AuthContext = react.createContext<AuthContextType>({} as AuthContextType)
|
const AuthContext = React.createContext<AuthContextType>({} as AuthContextType)
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
// === AuthProvider ===
|
// === AuthProvider ===
|
||||||
@ -167,7 +167,7 @@ export interface AuthProviderProps {
|
|||||||
authService: authServiceModule.AuthService
|
authService: authServiceModule.AuthService
|
||||||
/** Callback to execute once the user has authenticated successfully. */
|
/** Callback to execute once the user has authenticated successfully. */
|
||||||
onAuthenticated: () => void
|
onAuthenticated: () => void
|
||||||
children: react.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A React provider for the Cognito API. */
|
/** A React provider for the Cognito API. */
|
||||||
@ -187,25 +187,50 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
// and the function call would error.
|
// and the function call would error.
|
||||||
// eslint-disable-next-line no-restricted-properties
|
// eslint-disable-next-line no-restricted-properties
|
||||||
const navigate = router.useNavigate()
|
const navigate = router.useNavigate()
|
||||||
|
const [forceOfflineMode, setForceOfflineMode] = React.useState(shouldStartInOfflineMode)
|
||||||
|
const [initialized, setInitialized] = React.useState(false)
|
||||||
|
const [userSession, setUserSession] = React.useState<UserSession | null>(null)
|
||||||
|
|
||||||
const [forceOfflineMode, setForceOfflineMode] = react.useState(shouldStartInOfflineMode)
|
const goOfflineInternal = React.useCallback(() => {
|
||||||
const [initialized, setInitialized] = react.useState(false)
|
setInitialized(true)
|
||||||
const [userSession, setUserSession] = react.useState<UserSession | null>(null)
|
setUserSession(OFFLINE_USER_SESSION)
|
||||||
|
if (supportsLocalBackend) {
|
||||||
|
setBackendWithoutSavingType(new localBackend.LocalBackend())
|
||||||
|
} else {
|
||||||
|
// Provide dummy headers to avoid errors. This `Backend` will never be called as
|
||||||
|
// the entire UI will be disabled.
|
||||||
|
const client = new http.Client(new Headers([['Authorization', '']]))
|
||||||
|
setBackendWithoutSavingType(new remoteBackend.RemoteBackend(client, logger))
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
/* should never change */ supportsLocalBackend,
|
||||||
|
/* should never change */ logger,
|
||||||
|
/* should never change */ setBackendWithoutSavingType,
|
||||||
|
])
|
||||||
|
|
||||||
|
const goOffline = React.useCallback(() => {
|
||||||
|
toast.error('You are offline, switching to offline mode.')
|
||||||
|
goOfflineInternal()
|
||||||
|
navigate(app.DASHBOARD_PATH)
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}, [/* should never change */ goOfflineInternal, /* should never change */ navigate])
|
||||||
|
|
||||||
// This is identical to `hooks.useOnlineCheck`, however it is inline here to avoid any possible
|
// This is identical to `hooks.useOnlineCheck`, however it is inline here to avoid any possible
|
||||||
// circular dependency.
|
// circular dependency.
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
// `navigator.onLine` is not a dependency so that the app doesn't make the remote backend
|
||||||
|
// completely unusable on unreliable connections.
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
void goOffline()
|
void goOffline()
|
||||||
}
|
}
|
||||||
}, [navigator.onLine])
|
}, [goOffline])
|
||||||
|
|
||||||
/** Fetch the JWT access token from the session via the AWS Amplify library.
|
/** Fetch the JWT access token from the session via the AWS Amplify library.
|
||||||
*
|
*
|
||||||
* When invoked, retrieves the access token (if available) from the storage method chosen when
|
* When invoked, retrieves the access token (if available) from the storage method chosen when
|
||||||
* Amplify was configured (e.g. local storage). If the token is not available, return `undefined`.
|
* Amplify was configured (e.g. local storage). If the token is not available, return `undefined`.
|
||||||
* If the token has expired, automatically refreshes the token and returns the new token. */
|
* If the token has expired, automatically refreshes the token and returns the new token. */
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchSession = async () => {
|
const fetchSession = async () => {
|
||||||
if (!navigator.onLine || forceOfflineMode) {
|
if (!navigator.onLine || forceOfflineMode) {
|
||||||
goOfflineInternal()
|
goOfflineInternal()
|
||||||
@ -280,7 +305,18 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
logger.error(error)
|
logger.error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [session])
|
// `userSession` MUST NOT be a dependency as this effect always does a `setUserSession`,
|
||||||
|
// which would result in an infinite loop.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
cognito,
|
||||||
|
initialized,
|
||||||
|
logger,
|
||||||
|
onAuthenticated,
|
||||||
|
session,
|
||||||
|
/* should never change */ goOfflineInternal,
|
||||||
|
/* should never change */ setBackendWithoutSavingType,
|
||||||
|
])
|
||||||
|
|
||||||
/** Wrap a function returning a {@link Promise} to displays a loading toast notification
|
/** Wrap a function returning a {@link Promise} to displays a loading toast notification
|
||||||
* until the returned {@link Promise} finishes loading. */
|
* until the returned {@link Promise} finishes loading. */
|
||||||
@ -297,26 +333,6 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const goOfflineInternal = () => {
|
|
||||||
setInitialized(true)
|
|
||||||
setUserSession(OFFLINE_USER_SESSION)
|
|
||||||
if (supportsLocalBackend) {
|
|
||||||
setBackendWithoutSavingType(new localBackend.LocalBackend())
|
|
||||||
} else {
|
|
||||||
// Provide dummy headers to avoid errors. This `Backend` will never be called as
|
|
||||||
// the entire UI will be disabled.
|
|
||||||
const client = new http.Client(new Headers([['Authorization', '']]))
|
|
||||||
setBackendWithoutSavingType(new remoteBackend.RemoteBackend(client, logger))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const goOffline = () => {
|
|
||||||
toast.error('You are offline, switching to offline mode.')
|
|
||||||
goOfflineInternal()
|
|
||||||
navigate(app.DASHBOARD_PATH)
|
|
||||||
return Promise.resolve(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const signUp = async (username: string, password: string, organizationId: string | null) => {
|
const signUp = async (username: string, password: string, organizationId: string | null) => {
|
||||||
const result = await cognito.signUp(username, password, organizationId)
|
const result = await cognito.signUp(username, password, organizationId)
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
@ -492,7 +508,7 @@ function isUserFacingError(value: unknown): value is UserFacingError {
|
|||||||
* Only the hook is exported, and not the context, because we only want to use the hook directly and
|
* Only the hook is exported, and not the context, because we only want to use the hook directly and
|
||||||
* never the context component. */
|
* never the context component. */
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
return react.useContext(AuthContext)
|
return React.useContext(AuthContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Provider for the {@link SessionContextType}, which contains information about the
|
/** @file Provider for the {@link SessionContextType}, which contains information about the
|
||||||
* currently authenticated user's session. */
|
* currently authenticated user's session. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as results from 'ts-results'
|
import * as results from 'ts-results'
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ interface SessionContextType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** See `AuthContext` for safety details. */
|
/** See `AuthContext` for safety details. */
|
||||||
const SessionContext = react.createContext<SessionContextType>(
|
const SessionContext = React.createContext<SessionContextType>(
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
{} as SessionContextType
|
{} as SessionContextType
|
||||||
)
|
)
|
||||||
@ -46,7 +46,7 @@ export interface SessionProviderProps {
|
|||||||
mainPageUrl: URL
|
mainPageUrl: URL
|
||||||
registerAuthEventListener: listen.ListenFunction
|
registerAuthEventListener: listen.ListenFunction
|
||||||
userSession: () => Promise<results.Option<cognito.UserSession>>
|
userSession: () => Promise<results.Option<cognito.UserSession>>
|
||||||
children: react.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A React provider for the session of the authenticated user. */
|
/** A React provider for the session of the authenticated user. */
|
||||||
@ -57,7 +57,7 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||||||
|
|
||||||
/** Flag used to avoid rendering child components until we've fetched the user's session at least
|
/** Flag used to avoid rendering child components until we've fetched the user's session at least
|
||||||
* once. Avoids flash of the login screen when the user is already logged in. */
|
* once. Avoids flash of the login screen when the user is already logged in. */
|
||||||
const [initialized, setInitialized] = react.useState(false)
|
const [initialized, setInitialized] = React.useState(false)
|
||||||
|
|
||||||
/** Register an async effect that will fetch the user's session whenever the `refresh` state is
|
/** Register an async effect that will fetch the user's session whenever the `refresh` state is
|
||||||
* set. This is useful when a user has just logged in (as their cached credentials are
|
* set. This is useful when a user has just logged in (as their cached credentials are
|
||||||
@ -78,7 +78,7 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||||||
*
|
*
|
||||||
* For example, if a user clicks the signout button, this will clear the user's session, which
|
* For example, if a user clicks the signout button, this will clear the user's session, which
|
||||||
* means we want the login screen to render (which is a child of this provider). */
|
* means we want the login screen to render (which is a child of this provider). */
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
/** Handle Cognito authentication events
|
/** Handle Cognito authentication events
|
||||||
* @throws {error.UnreachableCaseError} Never. */
|
* @throws {error.UnreachableCaseError} Never. */
|
||||||
const listener: listen.ListenerCallback = event => {
|
const listener: listen.ListenerCallback = event => {
|
||||||
@ -112,7 +112,7 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||||||
* cleaned up between renders. This must be done because the `useEffect` will be called
|
* cleaned up between renders. This must be done because the `useEffect` will be called
|
||||||
* multiple times during the lifetime of the component. */
|
* multiple times during the lifetime of the component. */
|
||||||
return cancel
|
return cancel
|
||||||
}, [registerAuthEventListener])
|
}, [doRefresh, mainPageUrl, registerAuthEventListener])
|
||||||
|
|
||||||
const deinitializeSession = () => {
|
const deinitializeSession = () => {
|
||||||
setInitialized(false)
|
setInitialized(false)
|
||||||
@ -131,5 +131,5 @@ export function SessionProvider(props: SessionProviderProps) {
|
|||||||
|
|
||||||
/** React context hook returning the session of the authenticated user. */
|
/** React context hook returning the session of the authenticated user. */
|
||||||
export function useSession() {
|
export function useSession() {
|
||||||
return react.useContext(SessionContext)
|
return React.useContext(SessionContext)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
* {@link router.Route}s require fully authenticated users (c.f.
|
* {@link router.Route}s require fully authenticated users (c.f.
|
||||||
* {@link authProvider.FullUserSession}). */
|
* {@link authProvider.FullUserSession}). */
|
||||||
|
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router-dom'
|
import * as router from 'react-router-dom'
|
||||||
import * as toast from 'react-hot-toast'
|
import * as toast from 'react-hot-toast'
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ function AppRouter(props: AppProps) {
|
|||||||
window.navigate = navigate
|
window.navigate = navigate
|
||||||
}
|
}
|
||||||
const mainPageUrl = new URL(window.location.href)
|
const mainPageUrl = new URL(window.location.href)
|
||||||
const authService = react.useMemo(() => {
|
const authService = React.useMemo(() => {
|
||||||
const authConfig = { navigate, ...props }
|
const authConfig = { navigate, ...props }
|
||||||
return authServiceModule.initAuthService(authConfig)
|
return authServiceModule.initAuthService(authConfig)
|
||||||
}, [navigate, props])
|
}, [navigate, props])
|
||||||
@ -162,7 +162,7 @@ function AppRouter(props: AppProps) {
|
|||||||
null!
|
null!
|
||||||
const routes = (
|
const routes = (
|
||||||
<router.Routes>
|
<router.Routes>
|
||||||
<react.Fragment>
|
<React.Fragment>
|
||||||
{/* Login & registration pages are visible to unauthenticated users. */}
|
{/* Login & registration pages are visible to unauthenticated users. */}
|
||||||
<router.Route element={<authProvider.GuestLayout />}>
|
<router.Route element={<authProvider.GuestLayout />}>
|
||||||
<router.Route path={REGISTRATION_PATH} element={<Registration />} />
|
<router.Route path={REGISTRATION_PATH} element={<Registration />} />
|
||||||
@ -183,7 +183,7 @@ function AppRouter(props: AppProps) {
|
|||||||
<router.Route path={CONFIRM_REGISTRATION_PATH} element={<ConfirmRegistration />} />
|
<router.Route path={CONFIRM_REGISTRATION_PATH} element={<ConfirmRegistration />} />
|
||||||
<router.Route path={FORGOT_PASSWORD_PATH} element={<ForgotPassword />} />
|
<router.Route path={FORGOT_PASSWORD_PATH} element={<ForgotPassword />} />
|
||||||
<router.Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
|
<router.Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
|
||||||
</react.Fragment>
|
</React.Fragment>
|
||||||
</router.Routes>
|
</router.Routes>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
/** @file File containing SVG icon definitions. */
|
/** @file File containing SVG icon definitions. */
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/342
|
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/342
|
||||||
* These should all be regular `.svg` files rather than React components, but React doesn't include
|
* These should all be regular `.svg` files rather than React components, but React doesn't include
|
||||||
* the `svg` files when building for Electron. Once the build scripts have been adapted to allow for
|
* the `svg` files when building for Electron. Once the build scripts have been adapted to allow for
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Managing the logic and displaying the UI for the password change function. */
|
/** @file Managing the logic and displaying the UI for the password change function. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
import ArrowRightIcon from 'enso-assets/arrow_right.svg'
|
||||||
@ -23,9 +23,9 @@ function ChangePasswordModal() {
|
|||||||
const { changePassword } = auth.useAuth()
|
const { changePassword } = auth.useAuth()
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
const [oldPassword, setOldPassword] = react.useState('')
|
const [oldPassword, setOldPassword] = React.useState('')
|
||||||
const [newPassword, setNewPassword] = react.useState('')
|
const [newPassword, setNewPassword] = React.useState('')
|
||||||
const [confirmNewPassword, setConfirmNewPassword] = react.useState('')
|
const [confirmNewPassword, setConfirmNewPassword] = React.useState('')
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (newPassword !== confirmNewPassword) {
|
if (newPassword !== confirmNewPassword) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/** @file Modal for confirming delete of any type of asset. */
|
/** @file Modal for confirming delete of any type of asset. */
|
||||||
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import CloseIcon from 'enso-assets/close.svg'
|
import CloseIcon from 'enso-assets/close.svg'
|
||||||
@ -61,7 +62,7 @@ function ConfirmDeleteModal(props: ConfirmDeleteModalProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
Are you sure you want to delete the {assetType} '{name}'?
|
Are you sure you want to delete the {assetType} ‘{name}’?
|
||||||
</div>
|
</div>
|
||||||
<div className="m-1">
|
<div className="m-1">
|
||||||
<button
|
<button
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file A context menu. */
|
/** @file A context menu. */
|
||||||
|
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// =================
|
// =================
|
||||||
// === Constants ===
|
// === Constants ===
|
||||||
@ -18,18 +18,18 @@ export interface ContextMenuProps {
|
|||||||
// `left: number` and `top: number` may be more correct,
|
// `left: number` and `top: number` may be more correct,
|
||||||
// however passing an event eliminates the chance
|
// however passing an event eliminates the chance
|
||||||
// of passing the wrong coordinates from the event.
|
// of passing the wrong coordinates from the event.
|
||||||
event: react.MouseEvent
|
event: React.MouseEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A context menu that opens at the current mouse position. */
|
/** A context menu that opens at the current mouse position. */
|
||||||
function ContextMenu(props: react.PropsWithChildren<ContextMenuProps>) {
|
function ContextMenu(props: React.PropsWithChildren<ContextMenuProps>) {
|
||||||
const { children, event } = props
|
const { children, event } = props
|
||||||
const contextMenuRef = react.useRef<HTMLDivElement>(null)
|
const contextMenuRef = React.useRef<HTMLDivElement>(null)
|
||||||
const [top, setTop] = react.useState(event.pageY)
|
const [top, setTop] = React.useState(event.pageY)
|
||||||
// This must be the original height before the returned element affects the `scrollHeight`.
|
// This must be the original height before the returned element affects the `scrollHeight`.
|
||||||
const [bodyHeight] = react.useState(document.body.scrollHeight)
|
const [bodyHeight] = React.useState(document.body.scrollHeight)
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (contextMenuRef.current != null) {
|
if (contextMenuRef.current != null) {
|
||||||
setTop(Math.min(top, bodyHeight - contextMenuRef.current.clientHeight))
|
setTop(Math.min(top, bodyHeight - contextMenuRef.current.clientHeight))
|
||||||
const boundingBox = contextMenuRef.current.getBoundingClientRect()
|
const boundingBox = contextMenuRef.current.getBoundingClientRect()
|
||||||
@ -38,7 +38,7 @@ function ContextMenu(props: react.PropsWithChildren<ContextMenuProps>) {
|
|||||||
scroll(scrollX, scrollY + scrollBy)
|
scroll(scrollX, scrollY + scrollBy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [children])
|
}, [bodyHeight, children, top])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file An entry in a context menu. */
|
/** @file An entry in a context menu. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// === ContextMenuEntry ===
|
// === ContextMenuEntry ===
|
||||||
@ -9,13 +9,13 @@ import * as react from 'react'
|
|||||||
export interface ContextMenuEntryProps {
|
export interface ContextMenuEntryProps {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
title?: string
|
title?: string
|
||||||
onClick: (event: react.MouseEvent<HTMLButtonElement>) => void
|
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// This component MUST NOT use `useState` because it is not rendered directly.
|
// This component MUST NOT use `useState` because it is not rendered directly.
|
||||||
/** An item in a `ContextMenu`. */
|
/** An item in a `ContextMenu`. */
|
||||||
function ContextMenuEntry(props: react.PropsWithChildren<ContextMenuEntryProps>) {
|
function ContextMenuEntry(props: React.PropsWithChildren<ContextMenuEntryProps>) {
|
||||||
const { children, disabled, title, onClick } = props
|
const { children, disabled = false, title, onClick } = props
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* This should never be used directly, but instead should be wrapped in a component
|
* This should never be used directly, but instead should be wrapped in a component
|
||||||
* that creates a specific asset type. */
|
* that creates a specific asset type. */
|
||||||
|
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import CloseIcon from 'enso-assets/close.svg'
|
import CloseIcon from 'enso-assets/close.svg'
|
||||||
|
|
||||||
@ -21,9 +21,9 @@ export interface CreateFormPassthroughProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** `CreateFormPassthroughProps`, plus props that should be defined in the wrapper component. */
|
/** `CreateFormPassthroughProps`, plus props that should be defined in the wrapper component. */
|
||||||
export interface CreateFormProps extends CreateFormPassthroughProps, react.PropsWithChildren {
|
export interface CreateFormProps extends CreateFormPassthroughProps, React.PropsWithChildren {
|
||||||
title: string
|
title: string
|
||||||
onSubmit: (event: react.FormEvent) => Promise<void>
|
onSubmit: (event: React.FormEvent) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A form to create an element. */
|
/** A form to create an element. */
|
||||||
@ -31,7 +31,7 @@ function CreateForm(props: CreateFormProps) {
|
|||||||
const { title, left, top, children, onSubmit: innerOnSubmit } = props
|
const { title, left, top, children, onSubmit: innerOnSubmit } = props
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
const onSubmit = async (event: react.FormEvent) => {
|
const onSubmit = async (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
await innerOnSubmit(event)
|
await innerOnSubmit(event)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Main dashboard component, responsible for listing user's projects as well as other
|
/** @file Main dashboard component, responsible for listing user's projects as well as other
|
||||||
* interactive components. */
|
* interactive components. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import ArrowRightSmallIcon from 'enso-assets/arrow_right_small.svg'
|
import ArrowRightSmallIcon from 'enso-assets/arrow_right_small.svg'
|
||||||
@ -279,7 +279,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
|
|
||||||
const [refresh, doRefresh] = hooks.useRefresh()
|
const [refresh, doRefresh] = hooks.useRefresh()
|
||||||
|
|
||||||
const [onDirectoryNextLoaded, setOnDirectoryNextLoaded] = react.useState<
|
const [onDirectoryNextLoaded, setOnDirectoryNextLoaded] = React.useState<
|
||||||
((assets: backendModule.Asset[]) => void) | null
|
((assets: backendModule.Asset[]) => void) | null
|
||||||
>(() =>
|
>(() =>
|
||||||
initialProjectName != null
|
initialProjectName != null
|
||||||
@ -299,66 +299,67 @@ function Dashboard(props: DashboardProps) {
|
|||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] =
|
const [nameOfProjectToImmediatelyOpen, setNameOfProjectToImmediatelyOpen] =
|
||||||
react.useState(initialProjectName)
|
React.useState(initialProjectName)
|
||||||
const [projectEvent, setProjectEvent] = react.useState<projectActionButton.ProjectEvent | null>(
|
const [projectEvent, setProjectEvent] = React.useState<projectActionButton.ProjectEvent | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
const [query, setQuery] = react.useState('')
|
const [query, setQuery] = React.useState('')
|
||||||
const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = react.useState(false)
|
const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = React.useState(false)
|
||||||
const [directoryId, setDirectoryId] = react.useState(
|
const [directoryId, setDirectoryId] = React.useState(
|
||||||
session.organization != null ? rootDirectoryId(session.organization.id) : null
|
session.organization != null ? rootDirectoryId(session.organization.id) : null
|
||||||
)
|
)
|
||||||
const [directoryStack, setDirectoryStack] = react.useState<
|
const [directoryStack, setDirectoryStack] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.directory>[]
|
backendModule.Asset<backendModule.AssetType.directory>[]
|
||||||
>([])
|
>([])
|
||||||
// Defined by the spec as `compact` by default, however it is not ready yet.
|
// Defined by the spec as `compact` by default, however it is not ready yet.
|
||||||
const [columnDisplayMode, setColumnDisplayMode] = react.useState(ColumnDisplayMode.release)
|
const [columnDisplayMode, setColumnDisplayMode] = React.useState(ColumnDisplayMode.release)
|
||||||
const [tab, setTab] = react.useState(Tab.dashboard)
|
const [tab, setTab] = React.useState(Tab.dashboard)
|
||||||
const [project, setProject] = react.useState<backendModule.Project | null>(null)
|
const [project, setProject] = React.useState<backendModule.Project | null>(null)
|
||||||
const [selectedAssets, setSelectedAssets] = react.useState<backendModule.Asset[]>([])
|
const [selectedAssets, setSelectedAssets] = React.useState<backendModule.Asset[]>([])
|
||||||
const [isFileBeingDragged, setIsFileBeingDragged] = react.useState(false)
|
const [isFileBeingDragged, setIsFileBeingDragged] = React.useState(false)
|
||||||
const [isScrollBarVisible, setIsScrollBarVisible] = react.useState(false)
|
const [isScrollBarVisible, setIsScrollBarVisible] = React.useState(false)
|
||||||
|
|
||||||
const [isLoadingAssets, setIsLoadingAssets] = react.useState(true)
|
const [isLoadingAssets, setIsLoadingAssets] = React.useState(true)
|
||||||
const [projectAssets, setProjectAssetsRaw] = react.useState<
|
const [projectAssets, setProjectAssetsRaw] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.project>[]
|
backendModule.Asset<backendModule.AssetType.project>[]
|
||||||
>([])
|
>([])
|
||||||
const [directoryAssets, setDirectoryAssetsRaw] = react.useState<
|
const [directoryAssets, setDirectoryAssetsRaw] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.directory>[]
|
backendModule.Asset<backendModule.AssetType.directory>[]
|
||||||
>([])
|
>([])
|
||||||
const [secretAssets, setSecretAssetsRaw] = react.useState<
|
const [secretAssets, setSecretAssetsRaw] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.secret>[]
|
backendModule.Asset<backendModule.AssetType.secret>[]
|
||||||
>([])
|
>([])
|
||||||
const [fileAssets, setFileAssetsRaw] = react.useState<
|
const [fileAssets, setFileAssetsRaw] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.file>[]
|
backendModule.Asset<backendModule.AssetType.file>[]
|
||||||
>([])
|
>([])
|
||||||
const [visibleProjectAssets, setVisibleProjectAssets] = react.useState<
|
const [visibleProjectAssets, setVisibleProjectAssets] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.project>[]
|
backendModule.Asset<backendModule.AssetType.project>[]
|
||||||
>([])
|
>([])
|
||||||
const [visibleDirectoryAssets, setVisibleDirectoryAssets] = react.useState<
|
const [visibleDirectoryAssets, setVisibleDirectoryAssets] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.directory>[]
|
backendModule.Asset<backendModule.AssetType.directory>[]
|
||||||
>([])
|
>([])
|
||||||
const [visibleSecretAssets, setVisibleSecretAssets] = react.useState<
|
const [visibleSecretAssets, setVisibleSecretAssets] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.secret>[]
|
backendModule.Asset<backendModule.AssetType.secret>[]
|
||||||
>([])
|
>([])
|
||||||
const [visibleFileAssets, setVisibleFileAssets] = react.useState<
|
const [visibleFileAssets, setVisibleFileAssets] = React.useState<
|
||||||
backendModule.Asset<backendModule.AssetType.file>[]
|
backendModule.Asset<backendModule.AssetType.file>[]
|
||||||
>([])
|
>([])
|
||||||
const [projectDatas, setProjectDatas] = react.useState<
|
const [projectDatas, setProjectDatas] = React.useState<
|
||||||
Record<backendModule.ProjectId, projectActionButton.ProjectData>
|
Record<backendModule.ProjectId, projectActionButton.ProjectData>
|
||||||
>({})
|
>({})
|
||||||
|
|
||||||
const isListingLocalDirectoryAndWillFail =
|
const isListingLocalDirectoryAndWillFail =
|
||||||
backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail
|
backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail
|
||||||
const isListingRemoteDirectoryAndWillFail =
|
const isListingRemoteDirectoryAndWillFail =
|
||||||
backend.type === backendModule.BackendType.remote && !session.organization?.isEnabled
|
backend.type === backendModule.BackendType.remote &&
|
||||||
|
session.organization?.isEnabled !== true
|
||||||
const isListingRemoteDirectoryWhileOffline =
|
const isListingRemoteDirectoryWhileOffline =
|
||||||
session.type === authProvider.UserSessionType.offline &&
|
session.type === authProvider.UserSessionType.offline &&
|
||||||
backend.type === backendModule.BackendType.remote
|
backend.type === backendModule.BackendType.remote
|
||||||
const directory = directoryStack[directoryStack.length - 1]
|
const directory = directoryStack[directoryStack.length - 1]
|
||||||
const parentDirectory = directoryStack[directoryStack.length - 2]
|
const parentDirectory = directoryStack[directoryStack.length - 2]
|
||||||
|
|
||||||
const switchToIdeTab = react.useCallback(() => {
|
const switchToIdeTab = React.useCallback(() => {
|
||||||
setTab(Tab.ide)
|
setTab(Tab.ide)
|
||||||
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
||||||
if (ideElement) {
|
if (ideElement) {
|
||||||
@ -367,7 +368,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const switchToDashboardTab = react.useCallback(() => {
|
const switchToDashboardTab = React.useCallback(() => {
|
||||||
setTab(Tab.dashboard)
|
setTab(Tab.dashboard)
|
||||||
doRefresh()
|
doRefresh()
|
||||||
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
const ideElement = document.getElementById(IDE_ELEMENT_ID)
|
||||||
@ -375,9 +376,9 @@ function Dashboard(props: DashboardProps) {
|
|||||||
ideElement.style.top = '-100vh'
|
ideElement.style.top = '-100vh'
|
||||||
ideElement.style.display = 'fixed'
|
ideElement.style.display = 'fixed'
|
||||||
}
|
}
|
||||||
}, [])
|
}, [doRefresh])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onProjectManagerLoadingFailed = () => {
|
const onProjectManagerLoadingFailed = () => {
|
||||||
setLoadingProjectManagerDidFail(true)
|
setLoadingProjectManagerDidFail(true)
|
||||||
}
|
}
|
||||||
@ -393,13 +394,13 @@ function Dashboard(props: DashboardProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail) {
|
if (backend.type === backendModule.BackendType.local && loadingProjectManagerDidFail) {
|
||||||
setIsLoadingAssets(false)
|
setIsLoadingAssets(false)
|
||||||
}
|
}
|
||||||
}, [isLoadingAssets, loadingProjectManagerDidFail, backend.type])
|
}, [isLoadingAssets, loadingProjectManagerDidFail, backend.type])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (
|
if (
|
||||||
supportsLocalBackend &&
|
supportsLocalBackend &&
|
||||||
localStorage.getItem(backendProvider.BACKEND_TYPE_KEY) !==
|
localStorage.getItem(backendProvider.BACKEND_TYPE_KEY) !==
|
||||||
@ -407,16 +408,16 @@ function Dashboard(props: DashboardProps) {
|
|||||||
) {
|
) {
|
||||||
setBackend(new localBackend.LocalBackend())
|
setBackend(new localBackend.LocalBackend())
|
||||||
}
|
}
|
||||||
}, [])
|
}, [setBackend, supportsLocalBackend])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
document.addEventListener('show-dashboard', switchToDashboardTab)
|
document.addEventListener('show-dashboard', switchToDashboardTab)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('show-dashboard', switchToDashboardTab)
|
document.removeEventListener('show-dashboard', switchToDashboardTab)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [switchToDashboardTab])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (projectEvent != null) {
|
if (projectEvent != null) {
|
||||||
setProjectEvent(null)
|
setProjectEvent(null)
|
||||||
}
|
}
|
||||||
@ -500,9 +501,9 @@ function Dashboard(props: DashboardProps) {
|
|||||||
setDirectoryStack([...directoryStack, directoryAsset])
|
setDirectoryStack([...directoryStack, directoryAsset])
|
||||||
}
|
}
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const cachedDirectoryStackJson = localStorage.getItem(DIRECTORY_STACK_KEY)
|
const cachedDirectoryStackJson = localStorage.getItem(DIRECTORY_STACK_KEY)
|
||||||
if (cachedDirectoryStackJson) {
|
if (cachedDirectoryStackJson != null) {
|
||||||
// The JSON was inserted by the code below, so it will always have the right type.
|
// The JSON was inserted by the code below, so it will always have the right type.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const cachedDirectoryStack: backendModule.Asset<backendModule.AssetType.directory>[] =
|
const cachedDirectoryStack: backendModule.Asset<backendModule.AssetType.directory>[] =
|
||||||
@ -515,7 +516,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (
|
if (
|
||||||
session.organization == null ||
|
session.organization == null ||
|
||||||
directoryId === rootDirectoryId(session.organization.id)
|
directoryId === rootDirectoryId(session.organization.id)
|
||||||
@ -524,7 +525,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
} else {
|
} else {
|
||||||
localStorage.setItem(DIRECTORY_STACK_KEY, JSON.stringify(directoryStack))
|
localStorage.setItem(DIRECTORY_STACK_KEY, JSON.stringify(directoryStack))
|
||||||
}
|
}
|
||||||
}, [directoryStack])
|
}, [directoryId, directoryStack, session.organization])
|
||||||
|
|
||||||
/** React components for the name column. */
|
/** React components for the name column. */
|
||||||
const nameRenderers: {
|
const nameRenderers: {
|
||||||
@ -714,7 +715,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const columnRenderer: Record<
|
const columnRenderer: Record<
|
||||||
Exclude<Column, Column.name>,
|
Exclude<Column, Column.name>,
|
||||||
(asset: backendModule.Asset) => react.ReactNode
|
(asset: backendModule.Asset) => React.ReactNode
|
||||||
> = {
|
> = {
|
||||||
[Column.lastModified]: asset =>
|
[Column.lastModified]: asset =>
|
||||||
asset.modifiedAt && <>{dateTime.formatDateTime(new Date(asset.modifiedAt))}</>,
|
asset.modifiedAt && <>{dateTime.formatDateTime(new Date(asset.modifiedAt))}</>,
|
||||||
@ -734,7 +735,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
[Column.labels]: () => {
|
[Column.labels]: () => {
|
||||||
// This is not a React component even though it contains JSX.
|
// This is not a React component even though it contains JSX.
|
||||||
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line no-restricted-syntax, @typescript-eslint/no-unused-vars
|
||||||
const onContextMenu = (event: react.MouseEvent) => {
|
const onContextMenu = (event: React.MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
setModal(() => (
|
setModal(() => (
|
||||||
@ -863,23 +864,30 @@ function Dashboard(props: DashboardProps) {
|
|||||||
<>{COLUMN_NAME[column]}</>
|
<>{COLUMN_NAME[column]}</>
|
||||||
)
|
)
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const queryRegex = new RegExp(regexEscape(query), 'i')
|
const queryRegex = new RegExp(regexEscape(query), 'i')
|
||||||
const doesItMatchQuery = (asset: backendModule.Asset) => queryRegex.test(asset.title)
|
const doesItMatchQuery = (asset: backendModule.Asset) => queryRegex.test(asset.title)
|
||||||
setVisibleProjectAssets(projectAssets.filter(doesItMatchQuery))
|
setVisibleProjectAssets(projectAssets.filter(doesItMatchQuery))
|
||||||
setVisibleDirectoryAssets(directoryAssets.filter(doesItMatchQuery))
|
setVisibleDirectoryAssets(directoryAssets.filter(doesItMatchQuery))
|
||||||
setVisibleSecretAssets(secretAssets.filter(doesItMatchQuery))
|
setVisibleSecretAssets(secretAssets.filter(doesItMatchQuery))
|
||||||
setVisibleFileAssets(fileAssets.filter(doesItMatchQuery))
|
setVisibleFileAssets(fileAssets.filter(doesItMatchQuery))
|
||||||
}, [query])
|
}, [directoryAssets, fileAssets, projectAssets, query, secretAssets])
|
||||||
|
|
||||||
react.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (isLoadingAssets) {
|
if (isLoadingAssets) {
|
||||||
document.body.style.overflowY = isScrollBarVisible ? 'scroll' : ''
|
document.body.style.overflowY = isScrollBarVisible ? 'scroll' : ''
|
||||||
} else {
|
} else {
|
||||||
document.body.style.overflowY = ''
|
document.body.style.overflowY = ''
|
||||||
setIsScrollBarVisible(document.body.scrollHeight > document.body.clientHeight)
|
setIsScrollBarVisible(document.body.scrollHeight > document.body.clientHeight)
|
||||||
}
|
}
|
||||||
}, [isLoadingAssets, projectAssets, directoryAssets, secretAssets, fileAssets])
|
}, [
|
||||||
|
isLoadingAssets,
|
||||||
|
projectAssets,
|
||||||
|
directoryAssets,
|
||||||
|
secretAssets,
|
||||||
|
fileAssets,
|
||||||
|
isScrollBarVisible,
|
||||||
|
])
|
||||||
|
|
||||||
const setAssets = (assets: backendModule.Asset[]) => {
|
const setAssets = (assets: backendModule.Asset[]) => {
|
||||||
const newProjectAssets = assets.filter(
|
const newProjectAssets = assets.filter(
|
||||||
@ -947,7 +955,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
[session.accessToken, directoryId, refresh, backend]
|
[session.accessToken, directoryId, refresh, backend]
|
||||||
)
|
)
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
setIsFileBeingDragged(false)
|
setIsFileBeingDragged(false)
|
||||||
}
|
}
|
||||||
@ -957,7 +965,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleEscapeKey = (event: react.KeyboardEvent<HTMLDivElement>) => {
|
const handleEscapeKey = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
event.key === 'Escape' &&
|
event.key === 'Escape' &&
|
||||||
!event.ctrlKey &&
|
!event.ctrlKey &&
|
||||||
@ -972,7 +980,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDropZone = (event: react.DragEvent<HTMLDivElement>) => {
|
const openDropZone = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (event.dataTransfer.types.includes('Files')) {
|
if (event.dataTransfer.types.includes('Files')) {
|
||||||
setIsFileBeingDragged(true)
|
setIsFileBeingDragged(true)
|
||||||
}
|
}
|
||||||
@ -984,7 +992,7 @@ function Dashboard(props: DashboardProps) {
|
|||||||
let highestProjectIndex = 0
|
let highestProjectIndex = 0
|
||||||
for (const projectAsset of projectAssets) {
|
for (const projectAsset of projectAssets) {
|
||||||
const projectIndex = projectNameTemplate.exec(projectAsset.title)?.groups?.projectIndex
|
const projectIndex = projectNameTemplate.exec(projectAsset.title)?.groups?.projectIndex
|
||||||
if (projectIndex) {
|
if (projectIndex != null) {
|
||||||
highestProjectIndex = Math.max(highestProjectIndex, parseInt(projectIndex, 10))
|
highestProjectIndex = Math.max(highestProjectIndex, parseInt(projectIndex, 10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Form to create a project. */
|
/** @file Form to create a project. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import * as backendModule from '../backend'
|
import * as backendModule from '../backend'
|
||||||
@ -23,13 +23,13 @@ function FileCreateForm(props: FileCreateFormProps) {
|
|||||||
const { directoryId, onSuccess, ...passThrough } = props
|
const { directoryId, onSuccess, ...passThrough } = props
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
const [name, setName] = react.useState<string | null>(null)
|
const [name, setName] = React.useState<string | null>(null)
|
||||||
const [file, setFile] = react.useState<File | null>(null)
|
const [file, setFile] = React.useState<File | null>(null)
|
||||||
|
|
||||||
if (backend.type === backendModule.BackendType.local) {
|
if (backend.type === backendModule.BackendType.local) {
|
||||||
return <></>
|
return <></>
|
||||||
} else {
|
} else {
|
||||||
const onSubmit = async (event: react.FormEvent) => {
|
const onSubmit = async (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
// TODO[sb]: Uploading a file may be a mistake when creating a new file.
|
// TODO[sb]: Uploading a file may be a mistake when creating a new file.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Container that launches the IDE. */
|
/** @file Container that launches the IDE. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as backendModule from '../backend'
|
import * as backendModule from '../backend'
|
||||||
import * as backendProvider from '../../providers/backend'
|
import * as backendProvider from '../../providers/backend'
|
||||||
@ -31,7 +31,7 @@ function Ide(props: IdeProps) {
|
|||||||
const { project, appRunner } = props
|
const { project, appRunner } = props
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const ideVersion =
|
const ideVersion =
|
||||||
project.ideVersion?.value ??
|
project.ideVersion?.value ??
|
||||||
@ -122,7 +122,10 @@ function Ide(props: IdeProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}, [project])
|
// The backend MUST NOT be a dependency, since the IDE should only be recreated when a new
|
||||||
|
// project is opened.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appRunner, project])
|
||||||
|
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
/** @file Input element with default event handlers. */
|
/** @file Input element with default event handlers. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// =============
|
// =============
|
||||||
// === Input ===
|
// === Input ===
|
||||||
// =============
|
// =============
|
||||||
|
|
||||||
/** Props for an {@link Input}. */
|
/** Props for an {@link Input}. */
|
||||||
export interface InputProps extends react.InputHTMLAttributes<HTMLInputElement> {
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
setValue: (value: string) => void
|
setValue: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file A label, which may be either user-defined, or a system warning message. */
|
/** @file A label, which may be either user-defined, or a system warning message. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import ExclamationIcon from 'enso-assets/exclamation.svg'
|
import ExclamationIcon from 'enso-assets/exclamation.svg'
|
||||||
|
|
||||||
@ -47,11 +47,11 @@ const STATUS_ICON: Record<Status, JSX.Element | null> = {
|
|||||||
/** Props for a {@link Label}. */
|
/** Props for a {@link Label}. */
|
||||||
export interface LabelProps {
|
export interface LabelProps {
|
||||||
status?: Status
|
status?: Status
|
||||||
onContextMenu?: react.MouseEventHandler<HTMLDivElement>
|
onContextMenu?: React.MouseEventHandler<HTMLDivElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A label, which may be either user-defined, or a system warning message. */
|
/** A label, which may be either user-defined, or a system warning message. */
|
||||||
function Label(props: react.PropsWithChildren<LabelProps>) {
|
function Label(props: React.PropsWithChildren<LabelProps>) {
|
||||||
const { status = Status.none, children, onContextMenu } = props
|
const { status = Status.none, children, onContextMenu } = props
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Base modal component that provides the full-screen element that blocks mouse events. */
|
/** @file Base modal component that provides the full-screen element that blocks mouse events. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as modalProvider from '../../providers/modal'
|
import * as modalProvider from '../../providers/modal'
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ import * as modalProvider from '../../providers/modal'
|
|||||||
// =================
|
// =================
|
||||||
|
|
||||||
/** Props for a {@link Modal}. */
|
/** Props for a {@link Modal}. */
|
||||||
export interface ModalProps extends react.PropsWithChildren {
|
export interface ModalProps extends React.PropsWithChildren {
|
||||||
centered?: boolean
|
centered?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ export interface ModalProps extends react.PropsWithChildren {
|
|||||||
* background transparency can be enabled with Tailwind's `bg-opacity` classes,
|
* background transparency can be enabled with Tailwind's `bg-opacity` classes,
|
||||||
* like `className="bg-opacity-50"` */
|
* like `className="bg-opacity-50"` */
|
||||||
function Modal(props: ModalProps) {
|
function Modal(props: ModalProps) {
|
||||||
const { children, centered, className } = props
|
const { children, centered = false, className } = props
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Colored border around icons and text indicating permissions. */
|
/** @file Colored border around icons and text indicating permissions. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// =============
|
// =============
|
||||||
// === Types ===
|
// === Types ===
|
||||||
@ -44,7 +44,7 @@ export type Permissions = AdminPermissions | OwnerPermissions | RegularPermissio
|
|||||||
// =================
|
// =================
|
||||||
|
|
||||||
/** Props for a {@link PermissionDisplay}. */
|
/** Props for a {@link PermissionDisplay}. */
|
||||||
export interface PermissionDisplayProps extends react.PropsWithChildren {
|
export interface PermissionDisplayProps extends React.PropsWithChildren {
|
||||||
permissions: Permissions
|
permissions: Permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file An interactive button displaying the status of a project. */
|
/** @file An interactive button displaying the status of a project. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import ArrowUpIcon from 'enso-assets/arrow_up.svg'
|
import ArrowUpIcon from 'enso-assets/arrow_up.svg'
|
||||||
@ -93,7 +93,7 @@ const SPINNER_CSS_CLASSES: Record<SpinnerState, string> = {
|
|||||||
export interface ProjectActionButtonProps {
|
export interface ProjectActionButtonProps {
|
||||||
project: backendModule.Asset<backendModule.AssetType.project>
|
project: backendModule.Asset<backendModule.AssetType.project>
|
||||||
projectData: ProjectData
|
projectData: ProjectData
|
||||||
setProjectData: react.Dispatch<react.SetStateAction<ProjectData>>
|
setProjectData: React.Dispatch<React.SetStateAction<ProjectData>>
|
||||||
appRunner: AppRunner | null
|
appRunner: AppRunner | null
|
||||||
event: ProjectEvent | null
|
event: ProjectEvent | null
|
||||||
/** Called when the project is opened via the {@link ProjectActionButton}. */
|
/** Called when the project is opened via the {@link ProjectActionButton}. */
|
||||||
@ -123,20 +123,20 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
(project.projectState.type === backendModule.ProjectState.opened ||
|
(project.projectState.type === backendModule.ProjectState.opened ||
|
||||||
project.projectState.type === backendModule.ProjectState.openInProgress)
|
project.projectState.type === backendModule.ProjectState.openInProgress)
|
||||||
|
|
||||||
const [state, setState] = react.useState(() => {
|
const [state, setState] = React.useState(() => {
|
||||||
if (shouldCheckIfActuallyOpen) {
|
if (shouldCheckIfActuallyOpen) {
|
||||||
return backendModule.ProjectState.created
|
return backendModule.ProjectState.created
|
||||||
} else {
|
} else {
|
||||||
return project.projectState.type
|
return project.projectState.type
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const [isCheckingStatus, setIsCheckingStatus] = react.useState(false)
|
const [isCheckingStatus, setIsCheckingStatus] = React.useState(false)
|
||||||
const [isCheckingResources, setIsCheckingResources] = react.useState(false)
|
const [isCheckingResources, setIsCheckingResources] = React.useState(false)
|
||||||
const [spinnerState, setSpinnerState] = react.useState(SPINNER_STATE[state])
|
const [spinnerState, setSpinnerState] = React.useState(SPINNER_STATE[state])
|
||||||
const [shouldOpenWhenReady, setShouldOpenWhenReady] = react.useState(false)
|
const [shouldOpenWhenReady, setShouldOpenWhenReady] = React.useState(false)
|
||||||
const [toastId, setToastId] = react.useState<string | null>(null)
|
const [toastId, setToastId] = React.useState<string | null>(null)
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (toastId != null) {
|
if (toastId != null) {
|
||||||
return () => {
|
return () => {
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
@ -146,27 +146,61 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
}
|
}
|
||||||
}, [toastId])
|
}, [toastId])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Ensure that the previous spinner state is visible for at least one frame.
|
// Ensure that the previous spinner state is visible for at least one frame.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setSpinnerState(SPINNER_STATE[state])
|
setSpinnerState(SPINNER_STATE[state])
|
||||||
})
|
})
|
||||||
}, [state])
|
}, [state])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (toastId != null && state !== backendModule.ProjectState.openInProgress) {
|
if (toastId != null && state !== backendModule.ProjectState.openInProgress) {
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state, toastId])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldCheckIfActuallyOpen) {
|
if (shouldCheckIfActuallyOpen) {
|
||||||
setState(backendModule.ProjectState.openInProgress)
|
setState(backendModule.ProjectState.openInProgress)
|
||||||
setIsCheckingResources(true)
|
setIsCheckingResources(true)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [shouldCheckIfActuallyOpen])
|
||||||
|
|
||||||
react.useEffect(() => {
|
const openProject = React.useCallback(async () => {
|
||||||
|
setState(backendModule.ProjectState.openInProgress)
|
||||||
|
try {
|
||||||
|
switch (backend.type) {
|
||||||
|
case backendModule.BackendType.remote:
|
||||||
|
setToastId(toast.loading(LOADING_MESSAGE))
|
||||||
|
await backend.openProject(project.id)
|
||||||
|
setProjectData(oldProjectData => ({ ...oldProjectData, isRunning: true }))
|
||||||
|
doRefresh()
|
||||||
|
setIsCheckingStatus(true)
|
||||||
|
break
|
||||||
|
case backendModule.BackendType.local:
|
||||||
|
await backend.openProject(project.id)
|
||||||
|
setProjectData(oldProjectData => ({ ...oldProjectData, isRunning: true }))
|
||||||
|
setState(oldState => {
|
||||||
|
if (oldState === backendModule.ProjectState.openInProgress) {
|
||||||
|
setTimeout(() => {
|
||||||
|
doRefresh()
|
||||||
|
}, 0)
|
||||||
|
return backendModule.ProjectState.opened
|
||||||
|
} else {
|
||||||
|
return oldState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setIsCheckingStatus(false)
|
||||||
|
setIsCheckingResources(false)
|
||||||
|
toast.error(`Error opening project '${project.title}'.`)
|
||||||
|
setState(backendModule.ProjectState.closed)
|
||||||
|
}
|
||||||
|
}, [backend, doRefresh, project.id, project.title, setProjectData])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case ProjectEventType.open: {
|
case ProjectEventType.open: {
|
||||||
@ -183,25 +217,27 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [event])
|
}, [event, openProject, project.id])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldOpenWhenReady && state === backendModule.ProjectState.opened) {
|
if (shouldOpenWhenReady && state === backendModule.ProjectState.opened) {
|
||||||
openIde()
|
openIde()
|
||||||
setShouldOpenWhenReady(false)
|
setShouldOpenWhenReady(false)
|
||||||
}
|
}
|
||||||
}, [shouldOpenWhenReady, state])
|
}, [openIde, shouldOpenWhenReady, state])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (
|
if (
|
||||||
backend.type === backendModule.BackendType.local &&
|
backend.type === backendModule.BackendType.local &&
|
||||||
project.id !== localBackend.LocalBackend.currentlyOpeningProjectId
|
project.id !== localBackend.LocalBackend.currentlyOpeningProjectId
|
||||||
) {
|
) {
|
||||||
setState(backendModule.ProjectState.closed)
|
setState(backendModule.ProjectState.closed)
|
||||||
}
|
}
|
||||||
}, [project, state, localBackend.LocalBackend.currentlyOpeningProjectId])
|
// `localBackend.LocalBackend.currentlyOpeningProjectId` is a mutable outer scope value.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [project, state, backend.type, localBackend.LocalBackend.currentlyOpeningProjectId])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isCheckingStatus) {
|
if (!isCheckingStatus) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@ -240,9 +276,9 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isCheckingStatus])
|
}, [backend, isCheckingStatus, project.id])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isCheckingResources) {
|
if (!isCheckingResources) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@ -285,7 +321,7 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isCheckingResources])
|
}, [backend, isCheckingResources, project.id])
|
||||||
|
|
||||||
const closeProject = async () => {
|
const closeProject = async () => {
|
||||||
onClose()
|
onClose()
|
||||||
@ -303,40 +339,6 @@ function ProjectActionButton(props: ProjectActionButtonProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openProject = async () => {
|
|
||||||
setState(backendModule.ProjectState.openInProgress)
|
|
||||||
try {
|
|
||||||
switch (backend.type) {
|
|
||||||
case backendModule.BackendType.remote:
|
|
||||||
setToastId(toast.loading(LOADING_MESSAGE))
|
|
||||||
await backend.openProject(project.id)
|
|
||||||
setProjectData(oldProjectData => ({ ...oldProjectData, isRunning: true }))
|
|
||||||
doRefresh()
|
|
||||||
setIsCheckingStatus(true)
|
|
||||||
break
|
|
||||||
case backendModule.BackendType.local:
|
|
||||||
await backend.openProject(project.id)
|
|
||||||
setProjectData(oldProjectData => ({ ...oldProjectData, isRunning: true }))
|
|
||||||
setState(oldState => {
|
|
||||||
if (oldState === backendModule.ProjectState.openInProgress) {
|
|
||||||
setTimeout(() => {
|
|
||||||
doRefresh()
|
|
||||||
}, 0)
|
|
||||||
return backendModule.ProjectState.opened
|
|
||||||
} else {
|
|
||||||
return oldState
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setIsCheckingStatus(false)
|
|
||||||
setIsCheckingResources(false)
|
|
||||||
toast.error(`Error opening project '${project.title}'.`)
|
|
||||||
setState(backendModule.ProjectState.closed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case null:
|
case null:
|
||||||
case backendModule.ProjectState.created:
|
case backendModule.ProjectState.created:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Modal for confirming delete of any type of asset. */
|
/** @file Modal for confirming delete of any type of asset. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import CloseIcon from 'enso-assets/close.svg'
|
import CloseIcon from 'enso-assets/close.svg'
|
||||||
@ -28,7 +28,7 @@ function RenameModal(props: RenameModalProps) {
|
|||||||
const { assetType, name, namePattern, title, doRename, onComplete } = props
|
const { assetType, name, namePattern, title, doRename, onComplete } = props
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
const [newName, setNewName] = react.useState<string | null>(null)
|
const [newName, setNewName] = React.useState<string | null>(null)
|
||||||
|
|
||||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -67,7 +67,7 @@ function RenameModal(props: RenameModalProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
What do you want to rename the {assetType} '{name}' to?
|
What do you want to rename the {assetType} ‘{name}’ to?
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2">
|
<div className="m-2">
|
||||||
<Input
|
<Input
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Table that projects an object into each column. */
|
/** @file Table that projects an object into each column. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as svg from '../../components/svg'
|
import * as svg from '../../components/svg'
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const SPINNER_LOADING_CLASSES = 'grow dasharray-75 duration-1000 ease-linear'
|
|||||||
export interface Column<T> {
|
export interface Column<T> {
|
||||||
id: string
|
id: string
|
||||||
heading: JSX.Element
|
heading: JSX.Element
|
||||||
render: (item: T, index: number) => react.ReactNode
|
render: (item: T, index: number) => React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================
|
// =================
|
||||||
@ -36,14 +36,14 @@ export interface RowsProps<T> {
|
|||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
placeholder: JSX.Element
|
placeholder: JSX.Element
|
||||||
columns: Column<T>[]
|
columns: Column<T>[]
|
||||||
onClick: (item: T, event: react.MouseEvent<HTMLTableRowElement>) => void
|
onClick: (item: T, event: React.MouseEvent<HTMLTableRowElement>) => void
|
||||||
onContextMenu: (item: T, event: react.MouseEvent<HTMLTableRowElement>) => void
|
onContextMenu: (item: T, event: React.MouseEvent<HTMLTableRowElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Table that projects an object into each column. */
|
/** Table that projects an object into each column. */
|
||||||
function Rows<T>(props: RowsProps<T>) {
|
function Rows<T>(props: RowsProps<T>) {
|
||||||
const { columns, items, isLoading, getKey, placeholder, onClick, onContextMenu } = props
|
const { columns, items, isLoading, getKey, placeholder, onClick, onContextMenu } = props
|
||||||
const [spinnerClasses, setSpinnerClasses] = react.useState(SPINNER_INITIAL_CLASSES)
|
const [spinnerClasses, setSpinnerClasses] = React.useState(SPINNER_INITIAL_CLASSES)
|
||||||
|
|
||||||
const headerRow = (
|
const headerRow = (
|
||||||
<tr>
|
<tr>
|
||||||
@ -58,7 +58,7 @@ function Rows<T>(props: RowsProps<T>) {
|
|||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// Ensure the spinner stays in the "initial" state for at least one frame.
|
// Ensure the spinner stays in the "initial" state for at least one frame.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Form to create a project. */
|
/** @file Form to create a project. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import * as backendModule from '../backend'
|
import * as backendModule from '../backend'
|
||||||
@ -24,15 +24,15 @@ function SecretCreateForm(props: SecretCreateFormProps) {
|
|||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
const { unsetModal } = modalProvider.useSetModal()
|
const { unsetModal } = modalProvider.useSetModal()
|
||||||
|
|
||||||
const [name, setName] = react.useState<string | null>(null)
|
const [name, setName] = React.useState<string | null>(null)
|
||||||
const [value, setValue] = react.useState<string | null>(null)
|
const [value, setValue] = React.useState<string | null>(null)
|
||||||
|
|
||||||
if (backend.type === backendModule.BackendType.local) {
|
if (backend.type === backendModule.BackendType.local) {
|
||||||
return <></>
|
return <></>
|
||||||
} else {
|
} else {
|
||||||
const onSubmit = async (event: react.FormEvent) => {
|
const onSubmit = async (event: React.FormEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!name) {
|
if (name == null || name === '') {
|
||||||
toast.error('Please provide a secret name.')
|
toast.error('Please provide a secret name.')
|
||||||
} else if (value == null) {
|
} else if (value == null) {
|
||||||
// Secret value explicitly can be empty.
|
// Secret value explicitly can be empty.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Renders the list of templates from which a project can be created. */
|
/** @file Renders the list of templates from which a project can be created. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import PlusCircledIcon from 'enso-assets/plus_circled.svg'
|
import PlusCircledIcon from 'enso-assets/plus_circled.svg'
|
||||||
import RotatingArrowIcon from 'enso-assets/rotating_arrow.svg'
|
import RotatingArrowIcon from 'enso-assets/rotating_arrow.svg'
|
||||||
@ -158,10 +158,10 @@ export interface TemplatesProps {
|
|||||||
function Templates(props: TemplatesProps) {
|
function Templates(props: TemplatesProps) {
|
||||||
const { onTemplateClick } = props
|
const { onTemplateClick } = props
|
||||||
|
|
||||||
const [shadowClass, setShadowClass] = react.useState(
|
const [shadowClass, setShadowClass] = React.useState(
|
||||||
window.innerWidth <= MAX_WIDTH_NEEDING_SCROLL ? ShadowClass.bottom : ShadowClass.none
|
window.innerWidth <= MAX_WIDTH_NEEDING_SCROLL ? ShadowClass.bottom : ShadowClass.none
|
||||||
)
|
)
|
||||||
const [isOpen, setIsOpen] = react.useState(() => {
|
const [isOpen, setIsOpen] = React.useState(() => {
|
||||||
/** This must not be in a `useEffect` as it would flash open for one frame.
|
/** This must not be in a `useEffect` as it would flash open for one frame.
|
||||||
* It can be in a `useLayoutEffect` but as that needs to be checked every re-render,
|
* It can be in a `useLayoutEffect` but as that needs to be checked every re-render,
|
||||||
* this is slightly more performant. */
|
* this is slightly more performant. */
|
||||||
@ -180,9 +180,9 @@ function Templates(props: TemplatesProps) {
|
|||||||
|
|
||||||
// This is incorrect, but SAFE, as its value will always be assigned before any hooks are run.
|
// This is incorrect, but SAFE, as its value will always be assigned before any hooks are run.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const containerRef = react.useRef<HTMLDivElement>(null!)
|
const containerRef = React.useRef<HTMLDivElement>(null!)
|
||||||
|
|
||||||
const toggleIsOpen = react.useCallback(() => {
|
const toggleIsOpen = React.useCallback(() => {
|
||||||
setIsOpen(oldIsOpen => !oldIsOpen)
|
setIsOpen(oldIsOpen => !oldIsOpen)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -210,14 +210,14 @@ function Templates(props: TemplatesProps) {
|
|||||||
setShadowClass(newShadowClass)
|
setShadowClass(newShadowClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
window.addEventListener('resize', updateShadowClass)
|
window.addEventListener('resize', updateShadowClass)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', updateShadowClass)
|
window.removeEventListener('resize', updateShadowClass)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
localStorage.setItem(IS_TEMPLATES_OPEN_KEY, JSON.stringify(isOpen))
|
localStorage.setItem(IS_TEMPLATES_OPEN_KEY, JSON.stringify(isOpen))
|
||||||
}, [isOpen])
|
}, [isOpen])
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file The top-bar of dashboard. */
|
/** @file The top-bar of dashboard. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import BarsIcon from 'enso-assets/bars.svg'
|
import BarsIcon from 'enso-assets/bars.svg'
|
||||||
import CloudIcon from 'enso-assets/cloud.svg'
|
import CloudIcon from 'enso-assets/cloud.svg'
|
||||||
@ -37,24 +37,24 @@ export interface TopBarProps {
|
|||||||
function TopBar(props: TopBarProps) {
|
function TopBar(props: TopBarProps) {
|
||||||
const { supportsLocalBackend, projectName, tab, toggleTab, setBackendType, query, setQuery } =
|
const { supportsLocalBackend, projectName, tab, toggleTab, setBackendType, query, setQuery } =
|
||||||
props
|
props
|
||||||
const [isUserMenuVisible, setIsUserMenuVisible] = react.useState(false)
|
const [isUserMenuVisible, setIsUserMenuVisible] = React.useState(false)
|
||||||
const { modal } = modalProvider.useModal()
|
const { modal } = modalProvider.useModal()
|
||||||
const { setModal, unsetModal } = modalProvider.useSetModal()
|
const { setModal, unsetModal } = modalProvider.useSetModal()
|
||||||
const { backend } = backendProvider.useBackend()
|
const { backend } = backendProvider.useBackend()
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
setIsUserMenuVisible(false)
|
setIsUserMenuVisible(false)
|
||||||
}
|
}
|
||||||
}, [modal])
|
}, [modal])
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isUserMenuVisible) {
|
if (isUserMenuVisible) {
|
||||||
setModal(() => <UserMenu />)
|
setModal(() => <UserMenu />)
|
||||||
} else {
|
} else {
|
||||||
unsetModal()
|
unsetModal()
|
||||||
}
|
}
|
||||||
}, [isUserMenuVisible])
|
}, [isUserMenuVisible, setModal, unsetModal])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex mx-2 h-8">
|
<div className="flex mx-2 h-8">
|
||||||
@ -87,8 +87,9 @@ function TopBar(props: TopBarProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center bg-label rounded-full pl-1
|
className={`flex items-center bg-label rounded-full pl-1 pr-2.5 mx-2 ${
|
||||||
pr-2.5 mx-2 ${projectName ? 'cursor-pointer' : 'opacity-50'}`}
|
projectName != null ? 'cursor-pointer' : 'opacity-50'
|
||||||
|
}`}
|
||||||
onClick={toggleTab}
|
onClick={toggleTab}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -127,6 +128,7 @@ function TopBar(props: TopBarProps) {
|
|||||||
<a
|
<a
|
||||||
href="https://discord.gg/enso"
|
href="https://discord.gg/enso"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
className="flex items-center bg-help rounded-full px-2.5 text-white mx-2"
|
className="flex items-center bg-help rounded-full px-2.5 text-white mx-2"
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap">help chat</span>
|
<span className="whitespace-nowrap">help chat</span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file The UserMenu component provides a dropdown menu of user actions and settings. */
|
/** @file The UserMenu component provides a dropdown menu of user actions and settings. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as app from '../../components/app'
|
import * as app from '../../components/app'
|
||||||
import * as auth from '../../authentication/providers/auth'
|
import * as auth from '../../authentication/providers/auth'
|
||||||
@ -20,8 +20,8 @@ export interface UserMenuItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** User menu item. */
|
/** User menu item. */
|
||||||
function UserMenuItem(props: react.PropsWithChildren<UserMenuItemProps>) {
|
function UserMenuItem(props: React.PropsWithChildren<UserMenuItemProps>) {
|
||||||
const { children, disabled, onClick } = props
|
const { children, disabled = false, onClick } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -56,7 +56,7 @@ function UserMenu() {
|
|||||||
const username: string | null =
|
const username: string | null =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-non-null-assertion
|
||||||
accessToken != null ? JSON.parse(atob(accessToken.split('.')[1]!)).username : null
|
accessToken != null ? JSON.parse(atob(accessToken.split('.')[1]!)).username : null
|
||||||
const canChangePassword = username != null ? !/^Github_|^Google_/.test(username) : null
|
const canChangePassword = username != null ? !/^Github_|^Google_/.test(username) : false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -34,6 +34,15 @@ export class LocalBackend implements Partial<backend.Backend> {
|
|||||||
readonly type = backend.BackendType.local
|
readonly type = backend.BackendType.local
|
||||||
private readonly projectManager = projectManager.ProjectManager.default()
|
private readonly projectManager = projectManager.ProjectManager.default()
|
||||||
|
|
||||||
|
/** Create a {@link LocalBackend}. */
|
||||||
|
constructor() {
|
||||||
|
if (IS_DEV_MODE) {
|
||||||
|
// @ts-expect-error This exists only for debugging purposes. It does not have types
|
||||||
|
// because it MUST NOT be used in this codebase.
|
||||||
|
window.localBackend = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Return a list of assets in a directory.
|
/** Return a list of assets in a directory.
|
||||||
*
|
*
|
||||||
* @throws An error if the JSON-RPC call fails. */
|
* @throws An error if the JSON-RPC call fails. */
|
||||||
@ -83,7 +92,9 @@ export class LocalBackend implements Partial<backend.Backend> {
|
|||||||
async createProject(body: backend.CreateProjectRequestBody): Promise<backend.CreatedProject> {
|
async createProject(body: backend.CreateProjectRequestBody): Promise<backend.CreatedProject> {
|
||||||
const project = await this.projectManager.createProject({
|
const project = await this.projectManager.createProject({
|
||||||
name: newtype.asNewtype<projectManager.ProjectName>(body.projectName),
|
name: newtype.asNewtype<projectManager.ProjectName>(body.projectName),
|
||||||
...(body.projectTemplateName ? { projectTemplate: body.projectTemplateName } : {}),
|
...(body.projectTemplateName != null
|
||||||
|
? { projectTemplate: body.projectTemplateName }
|
||||||
|
: {}),
|
||||||
missingComponentAction: projectManager.MissingComponentAction.install,
|
missingComponentAction: projectManager.MissingComponentAction.install,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
@ -161,6 +161,11 @@ export class RemoteBackend implements backend.Backend {
|
|||||||
if (!this.client.defaultHeaders.has('Authorization')) {
|
if (!this.client.defaultHeaders.has('Authorization')) {
|
||||||
return this.throw('Authorization header not set.')
|
return this.throw('Authorization header not set.')
|
||||||
} else {
|
} else {
|
||||||
|
if (IS_DEV_MODE) {
|
||||||
|
// @ts-expect-error This exists only for debugging purposes. It does not have types
|
||||||
|
// because it MUST NOT be used in this codebase.
|
||||||
|
window.remoteBackend = this
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,14 +218,14 @@ export class RemoteBackend implements backend.Backend {
|
|||||||
'?' +
|
'?' +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
...(query.parentId ? { parent_id: query.parentId } : {}),
|
...(query.parentId != null ? { parent_id: query.parentId } : {}),
|
||||||
}).toString()
|
}).toString()
|
||||||
)
|
)
|
||||||
if (!responseIsSuccessful(response)) {
|
if (!responseIsSuccessful(response)) {
|
||||||
if (response.status === STATUS_SERVER_ERROR) {
|
if (response.status === STATUS_SERVER_ERROR) {
|
||||||
// The directory is probably empty.
|
// The directory is probably empty.
|
||||||
return []
|
return []
|
||||||
} else if (query.parentId) {
|
} else if (query.parentId != null) {
|
||||||
return this.throw(`Unable to list directory with ID '${query.parentId}'.`)
|
return this.throw(`Unable to list directory with ID '${query.parentId}'.`)
|
||||||
} else {
|
} else {
|
||||||
return this.throw('Unable to list root directory.')
|
return this.throw('Unable to list root directory.')
|
||||||
@ -393,8 +398,8 @@ export class RemoteBackend implements backend.Backend {
|
|||||||
'?' +
|
'?' +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
...(params.fileName ? { file_name: params.fileName } : {}),
|
...(params.fileName != null ? { file_name: params.fileName } : {}),
|
||||||
...(params.fileId ? { file_id: params.fileId } : {}),
|
...(params.fileId != null ? { file_id: params.fileId } : {}),
|
||||||
...(params.parentDirectoryId
|
...(params.parentDirectoryId
|
||||||
? { parent_directory_id: params.parentDirectoryId }
|
? { parent_directory_id: params.parentDirectoryId }
|
||||||
: {}),
|
: {}),
|
||||||
@ -403,9 +408,9 @@ export class RemoteBackend implements backend.Backend {
|
|||||||
body
|
body
|
||||||
)
|
)
|
||||||
if (!responseIsSuccessful(response)) {
|
if (!responseIsSuccessful(response)) {
|
||||||
if (params.fileName) {
|
if (params.fileName != null) {
|
||||||
return this.throw(`Unable to upload file with name '${params.fileName}'.`)
|
return this.throw(`Unable to upload file with name '${params.fileName}'.`)
|
||||||
} else if (params.fileId) {
|
} else if (params.fileId != null) {
|
||||||
return this.throw(`Unable to upload file with ID '${params.fileId}'.`)
|
return this.throw(`Unable to upload file with ID '${params.fileId}'.`)
|
||||||
} else {
|
} else {
|
||||||
return this.throw('Unable to upload file.')
|
return this.throw('Unable to upload file.')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file Module containing common custom React hooks used throughout out Dashboard. */
|
/** @file Module containing common custom React hooks used throughout out Dashboard. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
import * as router from 'react-router'
|
import * as router from 'react-router'
|
||||||
|
|
||||||
import * as app from './components/app'
|
import * as app from './components/app'
|
||||||
@ -10,11 +10,14 @@ import * as loggerProvider from './providers/logger'
|
|||||||
// === useRefresh ===
|
// === useRefresh ===
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
|
/** An alias to make the purpose of the returned empty object clearer. */
|
||||||
|
export interface RefreshState {}
|
||||||
|
|
||||||
/** A hook that contains no state, and is used only to tell React when to re-render. */
|
/** A hook that contains no state, and is used only to tell React when to re-render. */
|
||||||
export function useRefresh() {
|
export function useRefresh() {
|
||||||
// Uses an empty object literal because every distinct literal
|
// Uses an empty object literal because every distinct literal
|
||||||
// is a new reference and therefore is not equal to any other object literal.
|
// is a new reference and therefore is not equal to any other object literal.
|
||||||
return react.useReducer(() => ({}), {})
|
return React.useReducer((): RefreshState => ({}), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
@ -33,41 +36,38 @@ export function useRefresh() {
|
|||||||
* Also see: https://stackoverflow.com/questions/61751728/asynchronous-calls-with-react-usememo.
|
* 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 initialValue - The initial value of the state controlled by this hook.
|
||||||
* @param fetch - The asynchronous function used to load 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 fetch.
|
* @param deps - The list of dependencies that, when updated, trigger the asynchronous effect.
|
||||||
* @returns The current value of the state controlled by this hook. */
|
* @returns The current value of the state controlled by this hook. */
|
||||||
export function useAsyncEffect<T>(
|
export function useAsyncEffect<T>(
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
fetch: (signal: AbortSignal) => Promise<T>,
|
asyncEffect: (signal: AbortSignal) => Promise<T>,
|
||||||
deps?: react.DependencyList
|
deps?: React.DependencyList
|
||||||
): T {
|
): T {
|
||||||
const logger = loggerProvider.useLogger()
|
const logger = loggerProvider.useLogger()
|
||||||
const [value, setValue] = react.useState<T>(initialValue)
|
const [value, setValue] = React.useState<T>(initialValue)
|
||||||
|
|
||||||
react.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const { signal } = controller
|
void asyncEffect(controller.signal).then(
|
||||||
|
result => {
|
||||||
/** Declare the async data fetching function. */
|
if (!controller.signal.aborted) {
|
||||||
const load = async () => {
|
setValue(result)
|
||||||
const result = await fetch(signal)
|
}
|
||||||
|
},
|
||||||
/** Set state with the result only if this effect has not been aborted. This prevents race
|
error => {
|
||||||
* conditions by making it so that only the latest async fetch will update the state on
|
logger.error('Error while fetching data:', error)
|
||||||
* completion. */
|
|
||||||
if (!signal.aborted) {
|
|
||||||
setValue(result)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
load().catch(error => {
|
|
||||||
logger.error('Error while fetching data', error)
|
|
||||||
})
|
|
||||||
/** Cancel any future `setValue` calls. */
|
/** Cancel any future `setValue` calls. */
|
||||||
return () => {
|
return () => {
|
||||||
controller.abort()
|
controller.abort()
|
||||||
}
|
}
|
||||||
}, deps)
|
// 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
|
return value
|
||||||
}
|
}
|
||||||
@ -100,28 +100,48 @@ export function useNavigate() {
|
|||||||
return navigate
|
return navigate
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================
|
// ================
|
||||||
|
// === useEvent ===
|
||||||
|
// ================
|
||||||
|
|
||||||
|
/** A wrapper around `useState` that sets its value to `null` after the current render. */
|
||||||
|
export function useEvent<T>(): [event: T | null, dispatchEvent: (value: T | null) => void] {
|
||||||
|
const [event, setEvent] = React.useState<T | null>(null)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (event != null) {
|
||||||
|
setEvent(null)
|
||||||
|
}
|
||||||
|
}, [event])
|
||||||
|
|
||||||
|
return [event, setEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// === Debug wrappers for built-in hooks ===
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
// `console.*` is allowed because these are for debugging purposes only.
|
||||||
|
/* eslint-disable no-restricted-properties */
|
||||||
|
|
||||||
// === useDebugState ===
|
// === useDebugState ===
|
||||||
// =====================
|
|
||||||
|
|
||||||
/** A modified `useState` that logs the old and new values when `setState` is called. */
|
/** A modified `useState` that logs the old and new values when `setState` is called. */
|
||||||
export function useDebugState<T>(
|
export function useDebugState<T>(
|
||||||
initialState: T | (() => T),
|
initialState: T | (() => T),
|
||||||
name?: string
|
name?: string
|
||||||
): [state: T, setState: (valueOrUpdater: react.SetStateAction<T>, source?: string) => void] {
|
): [state: T, setState: (valueOrUpdater: React.SetStateAction<T>, source?: string) => void] {
|
||||||
const [state, rawSetState] = react.useState(initialState)
|
const [state, rawSetState] = React.useState(initialState)
|
||||||
|
|
||||||
const description = name != null ? `state for '${name}'` : 'state'
|
const description = name != null ? `state for '${name}'` : 'state'
|
||||||
|
|
||||||
const setState = react.useCallback(
|
const setState = React.useCallback(
|
||||||
(valueOrUpdater: react.SetStateAction<T>, source?: string) => {
|
(valueOrUpdater: React.SetStateAction<T>, source?: string) => {
|
||||||
const fullDescription = `${description}${source != null ? ` from '${source}'` : ''}`
|
const fullDescription = `${description}${source != null ? ` from '${source}'` : ''}`
|
||||||
if (typeof valueOrUpdater === 'function') {
|
if (typeof valueOrUpdater === 'function') {
|
||||||
// This is UNSAFE, however React makes the same assumption.
|
// This is UNSAFE, however React makes the same assumption.
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const updater = valueOrUpdater as (prevState: T) => T
|
const updater = valueOrUpdater as (prevState: T) => T
|
||||||
// `console.*` is allowed because this is for debugging purposes only.
|
|
||||||
/* eslint-disable no-restricted-properties */
|
|
||||||
rawSetState(oldState => {
|
rawSetState(oldState => {
|
||||||
console.group(description)
|
console.group(description)
|
||||||
console.log(`Old ${fullDescription}:`, oldState)
|
console.log(`Old ${fullDescription}:`, oldState)
|
||||||
@ -140,11 +160,80 @@ export function useDebugState<T>(
|
|||||||
}
|
}
|
||||||
return valueOrUpdater
|
return valueOrUpdater
|
||||||
})
|
})
|
||||||
/* eslint-enable no-restricted-properties */
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[description]
|
||||||
)
|
)
|
||||||
|
|
||||||
return [state, setState]
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 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 */
|
||||||
|
@ -82,7 +82,7 @@ export class Client {
|
|||||||
mimetype?: string
|
mimetype?: string
|
||||||
) {
|
) {
|
||||||
const headers = new Headers(this.defaultHeaders)
|
const headers = new Headers(this.defaultHeaders)
|
||||||
if (payload) {
|
if (payload != null) {
|
||||||
const contentType = mimetype ?? 'application/json'
|
const contentType = mimetype ?? 'application/json'
|
||||||
headers.set('Content-Type', contentType)
|
headers.set('Content-Type', contentType)
|
||||||
}
|
}
|
||||||
@ -98,7 +98,7 @@ export class Client {
|
|||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
...(payload ? { body: payload } : {}),
|
...(payload != null ? { body: payload } : {}),
|
||||||
}) as Promise<ResponseWithTypedJson<T>>
|
}) as Promise<ResponseWithTypedJson<T>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
/** @file Authentication module used by Enso IDE & Cloud.
|
/** @file Authentication module used by Enso IDE & Cloud.
|
||||||
*
|
*
|
||||||
* This module declares the main DOM structure for the authentication/dashboard app. */
|
* This module declares the main DOM structure for the authentication/dashboard app. */
|
||||||
/** This import is unused in this file, but React doesn't work without it, under Electron. This is
|
|
||||||
* probably because it gets tree-shaken out of the bundle, so we need to explicitly import it.
|
|
||||||
* Unlike all other imports in this project, this one is not `camelCase`. We use `React` instead of
|
|
||||||
* `react` here. This is because if the import is named any differently then React doesn't get
|
|
||||||
* included in the final bundle. */
|
|
||||||
// It is safe to disable `no-restricted-syntax` because the `PascalCase` naming is required
|
|
||||||
// as per the above comment.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-restricted-syntax
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as reactDOM from 'react-dom/client'
|
import * as reactDOM from 'react-dom/client'
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Defines the React provider for the project manager `Backend`, along with hooks to use the
|
/** @file Defines the React provider for the project manager `Backend`, along with hooks to use the
|
||||||
* provider via the shared React context. */
|
* provider via the shared React context. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as common from 'enso-common'
|
import * as common from 'enso-common'
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export interface BackendContextType {
|
|||||||
|
|
||||||
// @ts-expect-error The default value will never be exposed
|
// @ts-expect-error The default value will never be exposed
|
||||||
// as `backend` will always be accessed using `useBackend`.
|
// as `backend` will always be accessed using `useBackend`.
|
||||||
const BackendContext = react.createContext<BackendContextType>(null)
|
const BackendContext = React.createContext<BackendContextType>(null)
|
||||||
|
|
||||||
/** Props for a {@link BackendProvider}. */
|
/** Props for a {@link BackendProvider}. */
|
||||||
export interface BackendProviderProps extends React.PropsWithChildren<object> {
|
export interface BackendProviderProps extends React.PropsWithChildren<object> {
|
||||||
@ -48,14 +48,14 @@ export interface BackendProviderProps extends React.PropsWithChildren<object> {
|
|||||||
/** A React Provider that lets components get and set the current backend. */
|
/** A React Provider that lets components get and set the current backend. */
|
||||||
export function BackendProvider(props: BackendProviderProps) {
|
export function BackendProvider(props: BackendProviderProps) {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
const [backend, setBackendWithoutSavingType] = react.useState<
|
const [backend, setBackendWithoutSavingType] = React.useState<
|
||||||
localBackend.LocalBackend | remoteBackend.RemoteBackend
|
localBackend.LocalBackend | remoteBackend.RemoteBackend
|
||||||
// This default value is UNSAFE, but must neither be `LocalBackend`, which may not be
|
// This default value is UNSAFE, but must neither be `LocalBackend`, which may not be
|
||||||
// available, not `RemoteBackend`, which does not work when not yet logged in.
|
// available, not `RemoteBackend`, which does not work when not yet logged in.
|
||||||
// Care must be taken to initialize the backend before its first usage.
|
// Care must be taken to initialize the backend before its first usage.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
>(null!)
|
>(null!)
|
||||||
const setBackend = react.useCallback((newBackend: AnyBackendAPI) => {
|
const setBackend = React.useCallback((newBackend: AnyBackendAPI) => {
|
||||||
setBackendWithoutSavingType(newBackend)
|
setBackendWithoutSavingType(newBackend)
|
||||||
localStorage.setItem(BACKEND_TYPE_KEY, newBackend.type)
|
localStorage.setItem(BACKEND_TYPE_KEY, newBackend.type)
|
||||||
}, [])
|
}, [])
|
||||||
@ -69,12 +69,12 @@ export function BackendProvider(props: BackendProviderProps) {
|
|||||||
|
|
||||||
/** Exposes a property to get the current backend. */
|
/** Exposes a property to get the current backend. */
|
||||||
export function useBackend() {
|
export function useBackend() {
|
||||||
const { backend } = react.useContext(BackendContext)
|
const { backend } = React.useContext(BackendContext)
|
||||||
return { backend }
|
return { backend }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Exposes a property to set the current backend. */
|
/** Exposes a property to set the current backend. */
|
||||||
export function useSetBackend() {
|
export function useSetBackend() {
|
||||||
const { setBackend, setBackendWithoutSavingType } = react.useContext(BackendContext)
|
const { setBackend, setBackendWithoutSavingType } = React.useContext(BackendContext)
|
||||||
return { setBackend, setBackendWithoutSavingType }
|
return { setBackend, setBackendWithoutSavingType }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/** @file Defines the React provider for the {@link Logger} interface, along with a hook to use the
|
/** @file Defines the React provider for the {@link Logger} interface, along with a hook to use the
|
||||||
* provider via the shared React context. */
|
* provider via the shared React context. */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// === Logger ===
|
// === Logger ===
|
||||||
@ -23,7 +23,7 @@ export interface Logger {
|
|||||||
|
|
||||||
/** See {@link AuthContext} for safety details. */
|
/** See {@link AuthContext} for safety details. */
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
const LoggerContext = react.createContext<Logger>({} as Logger)
|
const LoggerContext = React.createContext<Logger>({} as Logger)
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// === LoggerProvider ===
|
// === LoggerProvider ===
|
||||||
@ -31,7 +31,7 @@ const LoggerContext = react.createContext<Logger>({} as Logger)
|
|||||||
|
|
||||||
/** Props for a {@link LoggerProvider}. */
|
/** Props for a {@link LoggerProvider}. */
|
||||||
export interface LoggerProviderProps {
|
export interface LoggerProviderProps {
|
||||||
children: react.ReactNode
|
children: React.ReactNode
|
||||||
logger: Logger
|
logger: Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,5 +47,5 @@ export function LoggerProvider(props: LoggerProviderProps) {
|
|||||||
|
|
||||||
/** A React context hook exposing the diagnostic logger. */
|
/** A React context hook exposing the diagnostic logger. */
|
||||||
export function useLogger() {
|
export function useLogger() {
|
||||||
return react.useContext(LoggerContext)
|
return React.useContext(LoggerContext)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @file */
|
/** @file */
|
||||||
import * as react from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
// =============
|
// =============
|
||||||
// === Modal ===
|
// === Modal ===
|
||||||
@ -14,7 +14,7 @@ interface ModalContextType {
|
|||||||
setModal: (modal: Modal | null) => void
|
setModal: (modal: Modal | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalContext = react.createContext<ModalContextType>({
|
const ModalContext = React.createContext<ModalContextType>({
|
||||||
modal: null,
|
modal: null,
|
||||||
setModal: () => {
|
setModal: () => {
|
||||||
// Ignored. This default value will never be used
|
// Ignored. This default value will never be used
|
||||||
@ -28,19 +28,19 @@ export interface ModalProviderProps extends React.PropsWithChildren<object> {}
|
|||||||
/** A React provider containing the currently active modal. */
|
/** A React provider containing the currently active modal. */
|
||||||
export function ModalProvider(props: ModalProviderProps) {
|
export function ModalProvider(props: ModalProviderProps) {
|
||||||
const { children } = props
|
const { children } = props
|
||||||
const [modal, setModal] = react.useState<Modal | null>(null)
|
const [modal, setModal] = React.useState<Modal | null>(null)
|
||||||
return <ModalContext.Provider value={{ modal, setModal }}>{children}</ModalContext.Provider>
|
return <ModalContext.Provider value={{ modal, setModal }}>{children}</ModalContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A React context hook exposing the currently active modal, if one is currently visible. */
|
/** A React context hook exposing the currently active modal, if one is currently visible. */
|
||||||
export function useModal() {
|
export function useModal() {
|
||||||
const { modal } = react.useContext(ModalContext)
|
const { modal } = React.useContext(ModalContext)
|
||||||
return { modal }
|
return { modal }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A React context hook exposing functions to set and unset the currently active modal. */
|
/** A React context hook exposing functions to set and unset the currently active modal. */
|
||||||
export function useSetModal() {
|
export function useSetModal() {
|
||||||
const { setModal: setModalRaw } = react.useContext(ModalContext)
|
const { setModal: setModalRaw } = React.useContext(ModalContext)
|
||||||
const setModal = (modal: Modal) => {
|
const setModal = (modal: Modal) => {
|
||||||
setModalRaw(modal)
|
setModalRaw(modal)
|
||||||
}
|
}
|
||||||
|
76
app/ide-desktop/lib/types/modules.d.ts
vendored
76
app/ide-desktop/lib/types/modules.d.ts
vendored
@ -67,6 +67,82 @@ declare module 'eslint-plugin-jsdoc' {
|
|||||||
export default DEFAULT
|
export default DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'eslint-plugin-react' {
|
||||||
|
/** An ESLint configuration. */
|
||||||
|
interface Config {
|
||||||
|
plugins: string[]
|
||||||
|
rules: Record<string, number>
|
||||||
|
parserOptions: object
|
||||||
|
}
|
||||||
|
|
||||||
|
// The names come from a third-party API and cannot be changed.
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
/** Configurations defined by this ESLint plugin. */
|
||||||
|
interface Configs {
|
||||||
|
recommended: Config
|
||||||
|
all: Config
|
||||||
|
'jsx-runtime': Config
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deprecated rules contained in this ESLint plugin. */
|
||||||
|
interface DeprecatedRules {
|
||||||
|
'jsx-sort-default-props': object
|
||||||
|
'jsx-space-before-closing': object
|
||||||
|
}
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
|
/** The default export of this ESLint plugin. */
|
||||||
|
interface Default {
|
||||||
|
rules: Record<string, object>
|
||||||
|
configs: Configs
|
||||||
|
deprecatedRules: DeprecatedRules
|
||||||
|
}
|
||||||
|
|
||||||
|
// The names come from a third-party API and cannot be changed.
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
export const deprecatedRules: DeprecatedRules
|
||||||
|
|
||||||
|
const DEFAULT: Default
|
||||||
|
export default DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'eslint-plugin-react-hooks' {
|
||||||
|
/** An ESLint configuration. */
|
||||||
|
interface Config {
|
||||||
|
plugins: string[]
|
||||||
|
rules: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configurations defined by this ESLint plugin. */
|
||||||
|
interface Configs {
|
||||||
|
recommended: Config
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rules defined by this ESLint plugin. */
|
||||||
|
interface ReactHooksRules {
|
||||||
|
// The names come from a third-party API and cannot be changed.
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
'rules-of-hooks': object
|
||||||
|
'exhaustive-deps': object
|
||||||
|
/* eslint-enable @typescript-eslint/naming-convention */
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The default export of this ESLint plugin. */
|
||||||
|
interface Default {
|
||||||
|
configs: Configs
|
||||||
|
rules: ReactHooksRules
|
||||||
|
}
|
||||||
|
|
||||||
|
// The names come from a third-party API and cannot be changed.
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
export const configs: Configs
|
||||||
|
export const rules: ReactHooksRules
|
||||||
|
/* eslint-enable no-restricted-syntax */
|
||||||
|
|
||||||
|
const DEFAULT: Default
|
||||||
|
export default DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
declare module 'esbuild-plugin-time' {
|
declare module 'esbuild-plugin-time' {
|
||||||
import * as esbuild from 'esbuild'
|
import * as esbuild from 'esbuild'
|
||||||
|
|
||||||
|
394
app/ide-desktop/package-lock.json
generated
394
app/ide-desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -38,5 +38,9 @@
|
|||||||
"watch-dashboard": "npm run watch --workspace enso-dashboard",
|
"watch-dashboard": "npm run watch --workspace enso-dashboard",
|
||||||
"build-dashboard": "npm run build --workspace enso-dashboard",
|
"build-dashboard": "npm run build --workspace enso-dashboard",
|
||||||
"typecheck": "npx tsc -p lib/types/tsconfig.json && npm run typecheck --workspace enso && npm run typecheck --workspace enso-content && npm run typecheck --workspace enso-dashboard && npm run typecheck --workspace enso-authentication"
|
"typecheck": "npx tsc -p lib/types/tsconfig.json && npm run typecheck --workspace enso && npm run typecheck --workspace enso-content && npm run typecheck --workspace enso-dashboard && npm run typecheck --workspace enso-authentication"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user