Skip to content

Commit 857e874

Browse files
authored
Dashboard improvements (#8449)
- Closes enso-org/cloud-v2#799 - Pressing return in the label name input creates a label with a color defaulting the color used by the fewest number of tags - Right click on a label brings up a context menu - If you drag into a folder the folder should be expanded - this should work for all kinds of drag, including labels and assets - ⚠️ Drag & drop in Mac likely still does not work - I currently cannot test on macOS - Closes enso-org/cloud-v2#800 - Hide likes and the views on homepage view - Hide "New Project" (and other items that are not applicable) in context menu when on local backend - Download context menu option implemented for local mode - ⚠️ Copy not implemented as backend functionality does not yet exist. # Important Notes None
1 parent 3419c77 commit 857e874

22 files changed

+448
-177
lines changed

app/ide-desktop/lib/dashboard/src/authentication/src/authentication/cognito.ts

+19
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ export function intoSignUpErrorOrThrow(error: AmplifyError): SignUpError {
502502
/** Internal IDs of errors that may occur when confirming registration. */
503503
export enum ConfirmSignUpErrorKind {
504504
userAlreadyConfirmed = 'UserAlreadyConfirmed',
505+
userNotFound = 'UserNotFound',
505506
}
506507

507508
const CONFIRM_SIGN_UP_USER_ALREADY_CONFIRMED_ERROR = {
@@ -510,6 +511,13 @@ const CONFIRM_SIGN_UP_USER_ALREADY_CONFIRMED_ERROR = {
510511
kind: ConfirmSignUpErrorKind.userAlreadyConfirmed,
511512
}
512513

514+
const CONFIRM_SIGN_UP_USER_NOT_FOUND_ERROR = {
515+
internalCode: 'UserNotFoundException',
516+
internalMessage: 'Username/client id combination not found.',
517+
kind: ConfirmSignUpErrorKind.userNotFound,
518+
message: 'Incorrect email or confirmation code.',
519+
}
520+
513521
/** An error that may occur when confirming registration. */
514522
export interface ConfirmSignUpError extends CognitoError {
515523
kind: ConfirmSignUpErrorKind
@@ -531,6 +539,17 @@ export function intoConfirmSignUpErrorOrThrow(error: AmplifyError): ConfirmSignU
531539
kind: CONFIRM_SIGN_UP_USER_ALREADY_CONFIRMED_ERROR.kind,
532540
message: error.message,
533541
}
542+
} else if (
543+
error.code === CONFIRM_SIGN_UP_USER_NOT_FOUND_ERROR.internalCode &&
544+
error.message === CONFIRM_SIGN_UP_USER_NOT_FOUND_ERROR.internalMessage
545+
) {
546+
return {
547+
/** Don't re-use the original `error.code` here because Amplify overloads the same code
548+
* for multiple kinds of errors. We replace it with a custom code that has no
549+
* ambiguity. */
550+
kind: CONFIRM_SIGN_UP_USER_NOT_FOUND_ERROR.kind,
551+
message: CONFIRM_SIGN_UP_USER_NOT_FOUND_ERROR.message,
552+
}
534553
} else {
535554
throw error
536555
}

app/ide-desktop/lib/dashboard/src/authentication/src/authentication/components/registration.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import SubmitButton from './submitButton'
2323
// =================
2424

2525
const REGISTRATION_QUERY_PARAMS = {
26+
email: 'email',
2627
organizationId: 'organization_id',
2728
redirectTo: 'redirect_to',
2829
} as const
@@ -36,11 +37,11 @@ export default function Registration() {
3637
const auth = authModule.useAuth()
3738
const location = router.useLocation()
3839
const { localStorage } = localStorageProvider.useLocalStorage()
39-
const [email, setEmail] = React.useState('')
40+
const { email: urlEmail, organizationId, redirectTo } = parseUrlSearchParams(location.search)
41+
const [email, setEmail] = React.useState(urlEmail ?? '')
4042
const [password, setPassword] = React.useState('')
4143
const [confirmPassword, setConfirmPassword] = React.useState('')
4244
const [isSubmitting, setIsSubmitting] = React.useState(false)
43-
const { organizationId, redirectTo } = parseUrlSearchParams(location.search)
4445

4546
React.useEffect(() => {
4647
if (redirectTo != null) {
@@ -111,7 +112,8 @@ export default function Registration() {
111112
/** Return an object containing the query parameters, with keys renamed to `camelCase`. */
112113
function parseUrlSearchParams(search: string) {
113114
const query = new URLSearchParams(search)
115+
const email = query.get(REGISTRATION_QUERY_PARAMS.email)
114116
const organizationId = query.get(REGISTRATION_QUERY_PARAMS.organizationId)
115117
const redirectTo = query.get(REGISTRATION_QUERY_PARAMS.redirectTo)
116-
return { organizationId, redirectTo }
118+
return { email, organizationId, redirectTo }
117119
}

app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const REQUEST_DELAY_MS = 200
3535
const MESSAGES = {
3636
signUpSuccess: 'We have sent you an email with further instructions!',
3737
confirmSignUpSuccess: 'Your account has been confirmed! Please log in.',
38+
confirmSignUpFailure: 'Incorrect email or confirmation code.',
3839
setUsernameLoading: 'Setting username...',
3940
setUsernameSuccess: 'Your username has been set!',
4041
setUsernameFailure: 'Could not set your username.',
@@ -426,6 +427,10 @@ export function AuthProvider(props: AuthProviderProps) {
426427
switch (result.val.kind) {
427428
case cognitoModule.ConfirmSignUpErrorKind.userAlreadyConfirmed:
428429
break
430+
case cognitoModule.ConfirmSignUpErrorKind.userNotFound:
431+
toastError(MESSAGES.confirmSignUpFailure)
432+
navigate(app.LOGIN_PATH)
433+
return false
429434
default:
430435
throw new errorModule.UnreachableCaseError(result.val.kind)
431436
}
@@ -442,7 +447,8 @@ export function AuthProvider(props: AuthProviderProps) {
442447
toastSuccess(MESSAGES.signInWithPasswordSuccess)
443448
} else {
444449
if (result.val.kind === cognitoModule.SignInWithPasswordErrorKind.userNotFound) {
445-
navigate(app.REGISTRATION_PATH)
450+
// It may not be safe to pass the user's password in the URL.
451+
navigate(app.REGISTRATION_PATH + '?' + new URLSearchParams({ email }).toString())
446452
}
447453
toastError(result.val.message)
448454
}

app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/backend.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ export interface LChColor {
347347
}
348348

349349
/** A pre-selected list of colors to be used in color pickers. */
350-
export const COLORS: readonly LChColor[] = [
350+
export const COLORS: readonly [LChColor, ...LChColor[]] = [
351351
/* eslint-disable @typescript-eslint/no-magic-numbers */
352352
// Red
353353
{ lightness: 50, chroma: 66, hue: 7 },
@@ -379,6 +379,24 @@ export function lChColorToCssColor(color: LChColor): string {
379379
: `lch(${color.lightness}% ${color.chroma} ${color.hue})`
380380
}
381381

382+
export const COLOR_STRING_TO_COLOR = new Map(
383+
COLORS.map(color => [lChColorToCssColor(color), color])
384+
)
385+
386+
export const INITIAL_COLOR_COUNTS = new Map(COLORS.map(color => [lChColorToCssColor(color), 0]))
387+
388+
/** The color that is used for the least labels. Ties are broken by order. */
389+
export function leastUsedColor(labels: Iterable<Label>) {
390+
const colorCounts = new Map(INITIAL_COLOR_COUNTS)
391+
for (const label of labels) {
392+
const colorString = lChColorToCssColor(label.color)
393+
colorCounts.set(colorString, (colorCounts.get(colorString) ?? 0) + 1)
394+
}
395+
const min = Math.min(...colorCounts.values())
396+
const [minColor] = [...colorCounts.entries()].find(kv => kv[1] === min) ?? []
397+
return minColor == null ? COLORS[0] : COLOR_STRING_TO_COLOR.get(minColor) ?? COLORS[0]
398+
}
399+
382400
// =================
383401
// === AssetType ===
384402
// =================

app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/column.tsx

+45-24
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as dateTime from './dateTime'
2020
import * as hooks from '../hooks'
2121
import * as modalProvider from '../providers/modal'
2222
import * as permissions from './permissions'
23+
import * as shortcuts from './shortcuts'
2324
import * as sorting from './sorting'
2425
import type * as tableColumn from './components/tableColumn'
2526
import * as uniqueString from '../uniqueString'
@@ -28,8 +29,11 @@ import type * as assetsTable from './components/assetsTable'
2829
import * as categorySwitcher from './components/categorySwitcher'
2930
import Label, * as labelModule from './components/label'
3031
import AssetNameColumn from './components/assetNameColumn'
32+
import ContextMenu from './components/contextMenu'
33+
import ContextMenus from './components/contextMenus'
3134
import ManageLabelsModal from './components/manageLabelsModal'
3235
import ManagePermissionsModal from './components/managePermissionsModal'
36+
import MenuEntry from './components/menuEntry'
3337
import PermissionDisplay from './components/permissionDisplay'
3438
import SvgMask from '../authentication/components/svgMask'
3539

@@ -258,7 +262,7 @@ function LabelsColumn(props: AssetColumnProps) {
258262
rowState: { temporarilyAddedLabels, temporarilyRemovedLabels },
259263
} = props
260264
const session = authProvider.useNonPartialUserSession()
261-
const { setModal } = modalProvider.useSetModal()
265+
const { setModal, unsetModal } = modalProvider.useSetModal()
262266
const { backend } = backendProvider.useBackend()
263267
const toastAndLog = hooks.useToastAndLog()
264268
const [isHovered, setIsHovered] = React.useState(false)
@@ -308,29 +312,46 @@ function LabelsColumn(props: AssetColumnProps) {
308312
onContextMenu={event => {
309313
event.preventDefault()
310314
event.stopPropagation()
311-
setAsset(oldAsset => {
312-
const newLabels =
313-
oldAsset.labels?.filter(oldLabel => oldLabel !== label) ?? []
314-
void backend
315-
.associateTag(asset.id, newLabels, asset.title)
316-
.catch(error => {
317-
toastAndLog(null, error)
318-
setAsset(oldAsset2 =>
319-
oldAsset2.labels?.some(
320-
oldLabel => oldLabel === label
321-
) === true
322-
? oldAsset2
323-
: {
324-
...oldAsset2,
325-
labels: [...(oldAsset2.labels ?? []), label],
326-
}
327-
)
328-
})
329-
return {
330-
...oldAsset,
331-
labels: newLabels,
332-
}
333-
})
315+
const doDelete = () => {
316+
unsetModal()
317+
setAsset(oldAsset => {
318+
const newLabels =
319+
oldAsset.labels?.filter(oldLabel => oldLabel !== label) ??
320+
[]
321+
void backend
322+
.associateTag(asset.id, newLabels, asset.title)
323+
.catch(error => {
324+
toastAndLog(null, error)
325+
setAsset(oldAsset2 =>
326+
oldAsset2.labels?.some(
327+
oldLabel => oldLabel === label
328+
) === true
329+
? oldAsset2
330+
: {
331+
...oldAsset2,
332+
labels: [
333+
...(oldAsset2.labels ?? []),
334+
label,
335+
],
336+
}
337+
)
338+
})
339+
return {
340+
...oldAsset,
341+
labels: newLabels,
342+
}
343+
})
344+
}
345+
setModal(
346+
<ContextMenus key={`label-${label}`} event={event}>
347+
<ContextMenu>
348+
<MenuEntry
349+
action={shortcuts.KeyboardAction.delete}
350+
doAction={doDelete}
351+
/>
352+
</ContextMenu>
353+
</ContextMenus>
354+
)
334355
}}
335356
onClick={event => {
336357
event.preventDefault()

0 commit comments

Comments
 (0)