mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 08:08:34 +03:00
Expose backend API to IDE (#10442)
Move `Backend` and supporting APIs from `dashboard` to `enso-common`. Closes #10400. # Important Notes - The utility modules required by `Backend` have been moved to `enso-common`. Those defining general-purpose helpers for working with standard types are now in a submodule `utilities/data` to match the IDE organization; in the future we can merge them with the `util/data` gui2 subtree. Moved utilities are reexported from their dashboard locations, so that moved and not-yet-moved modules can be imported consistently. - The `text` module has been moved to `enso-common`; the IDE doesn't have any localization mechanism yet, so we can share this one.
This commit is contained in:
parent
60c1a0e1f6
commit
32e843c614
@ -11,7 +11,14 @@
|
||||
"./src/detect": "./src/detect.ts",
|
||||
"./src/gtag": "./src/gtag.ts",
|
||||
"./src/load": "./src/load.ts",
|
||||
"./src/queryClient": "./src/queryClient.ts"
|
||||
"./src/queryClient": "./src/queryClient.ts",
|
||||
"./src/utilities/data/array": "./src/utilities/data/array.ts",
|
||||
"./src/utilities/data/dateTime": "./src/utilities/data/dateTime.ts",
|
||||
"./src/utilities/data/newtype": "./src/utilities/data/newtype.ts",
|
||||
"./src/utilities/uniqueString": "./src/utilities/uniqueString.ts",
|
||||
"./src/text": "./src/text/index.ts",
|
||||
"./src/utilities/permissions": "./src/utilities/permissions.ts",
|
||||
"./src/services/Backend": "./src/services/Backend.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/query-core": "5.45.0",
|
||||
|
1473
app/ide-desktop/lib/common/src/services/Backend.ts
Normal file
1473
app/ide-desktop/lib/common/src/services/Backend.ts
Normal file
File diff suppressed because it is too large
Load Diff
136
app/ide-desktop/lib/common/src/text/index.ts
Normal file
136
app/ide-desktop/lib/common/src/text/index.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/** @file Functions related to displaying text. */
|
||||
|
||||
import ENGLISH from './english.json' assert { type: 'json' }
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
// =============
|
||||
|
||||
/** Possible languages in which to display text. */
|
||||
export enum Language {
|
||||
english = 'english',
|
||||
}
|
||||
|
||||
export const LANGUAGE_TO_LOCALE: Record<Language, string> = {
|
||||
[Language.english]: 'en-US',
|
||||
}
|
||||
|
||||
/** An object containing the corresponding localized text for each text ID. */
|
||||
type Texts = typeof ENGLISH
|
||||
/** All possible text IDs. */
|
||||
export type TextId = keyof Texts
|
||||
|
||||
/** Overrides the default number of placeholders (0). */
|
||||
interface PlaceholderOverrides {
|
||||
readonly copyAssetError: [assetName: string]
|
||||
readonly moveAssetError: [assetName: string]
|
||||
readonly findProjectError: [projectName: string]
|
||||
readonly openProjectError: [projectName: string]
|
||||
readonly deleteAssetError: [assetName: string]
|
||||
readonly restoreAssetError: [assetName: string]
|
||||
readonly restoreProjectError: [projectName: string]
|
||||
readonly unknownThreadIdError: [threadId: string]
|
||||
readonly needsOwnerError: [assetType: string]
|
||||
readonly inviteSuccess: [userEmail: string]
|
||||
readonly inviteManyUsersSuccess: [userCount: number]
|
||||
|
||||
readonly deleteLabelActionText: [labelName: string]
|
||||
readonly deleteSelectedAssetActionText: [assetName: string]
|
||||
readonly deleteSelectedAssetsActionText: [count: number]
|
||||
readonly deleteSelectedAssetForeverActionText: [assetName: string]
|
||||
readonly deleteSelectedAssetsForeverActionText: [count: number]
|
||||
readonly deleteUserActionText: [userName: string]
|
||||
readonly deleteUserGroupActionText: [groupName: string]
|
||||
readonly removeUserFromUserGroupActionText: [userName: string, groupName: string]
|
||||
readonly confirmPrompt: [action: string]
|
||||
readonly deleteTheAssetTypeTitle: [assetType: string, assetName: string]
|
||||
readonly couldNotInviteUser: [userEmail: string]
|
||||
readonly filesWithoutConflicts: [fileCount: number]
|
||||
readonly projectsWithoutConflicts: [projectCount: number]
|
||||
readonly andOtherFiles: [fileCount: number]
|
||||
readonly andOtherProjects: [projectCount: number]
|
||||
readonly emailIsNotAValidEmail: [userEmail: string]
|
||||
readonly userIsAlreadyInTheOrganization: [userEmail: string]
|
||||
readonly youAreAlreadyAddingUser: [userEmail: string]
|
||||
readonly lastModifiedOn: [dateString: string]
|
||||
readonly versionX: [version: number | string]
|
||||
readonly buildX: [build: string]
|
||||
readonly electronVersionX: [electronVersion: string]
|
||||
readonly chromeVersionX: [chromeVersion: string]
|
||||
readonly userAgentX: [userAgent: string]
|
||||
readonly compareVersionXWithLatest: [versionNumber: number]
|
||||
readonly onDateX: [dateString: string]
|
||||
readonly xUsersAndGroupsSelected: [usersAndGroupsCount: number]
|
||||
readonly upgradeTo: [planName: string]
|
||||
readonly enterTheNewKeyboardShortcutFor: [actionName: string]
|
||||
readonly downloadProjectError: [projectName: string]
|
||||
readonly downloadFileError: [fileName: string]
|
||||
readonly downloadDatalinkError: [datalinkName: string]
|
||||
readonly deleteUserGroupError: [userGroupName: string]
|
||||
readonly removeUserFromUserGroupError: [userName: string, userGroupName: string]
|
||||
readonly deleteUserError: [userName: string]
|
||||
|
||||
readonly inviteUserBackendError: [string]
|
||||
readonly changeUserGroupsBackendError: [string]
|
||||
readonly listFolderBackendError: [string]
|
||||
readonly createFolderBackendError: [string]
|
||||
readonly updateFolderBackendError: [string]
|
||||
readonly listAssetVersionsBackendError: [string]
|
||||
readonly getFileContentsBackendError: [string]
|
||||
readonly updateAssetBackendError: [string]
|
||||
readonly deleteAssetBackendError: [string]
|
||||
readonly undoDeleteAssetBackendError: [string]
|
||||
readonly copyAssetBackendError: [string, string]
|
||||
readonly createProjectBackendError: [string]
|
||||
readonly restoreProjectBackendError: [string]
|
||||
readonly duplicateProjectBackendError: [string]
|
||||
readonly closeProjectBackendError: [string]
|
||||
readonly listProjectSessionsBackendError: [string]
|
||||
readonly getProjectDetailsBackendError: [string]
|
||||
readonly getProjectLogsBackendError: [string]
|
||||
readonly openProjectBackendError: [string]
|
||||
readonly openProjectMissingCredentialsBackendError: [string]
|
||||
readonly updateProjectBackendError: [string]
|
||||
readonly checkResourcesBackendError: [string]
|
||||
readonly uploadFileWithNameBackendError: [string]
|
||||
readonly getFileDetailsBackendError: [string]
|
||||
readonly createDatalinkBackendError: [string]
|
||||
readonly getDatalinkBackendError: [string]
|
||||
readonly deleteDatalinkBackendError: [string]
|
||||
readonly createSecretBackendError: [string]
|
||||
readonly getSecretBackendError: [string]
|
||||
readonly updateSecretBackendError: [string]
|
||||
readonly createLabelBackendError: [string]
|
||||
readonly associateLabelsBackendError: [string]
|
||||
readonly deleteLabelBackendError: [string]
|
||||
readonly createUserGroupBackendError: [string]
|
||||
readonly deleteUserGroupBackendError: [string]
|
||||
readonly listVersionsBackendError: [string]
|
||||
readonly createCheckoutSessionBackendError: [string]
|
||||
readonly getCheckoutSessionBackendError: [string]
|
||||
readonly getDefaultVersionBackendError: [string]
|
||||
readonly logEventBackendError: [string]
|
||||
|
||||
readonly subscribeSuccessSubtitle: [string]
|
||||
readonly assetsDropFilesDescription: [count: number]
|
||||
|
||||
readonly paywallAvailabilityLevel: [plan: string]
|
||||
readonly paywallScreenDescription: [plan: string]
|
||||
readonly userGroupsLimitMessage: [limit: number]
|
||||
readonly inviteFormSeatsLeftError: [exceedBy: number]
|
||||
readonly inviteFormSeatsLeft: [seatsLeft: number]
|
||||
readonly seatsLeft: [seatsLeft: number, seatsTotal: number]
|
||||
}
|
||||
|
||||
/** An tuple of `string` for placeholders for each {@link TextId}. */
|
||||
export interface Replacements
|
||||
extends PlaceholderOverrides,
|
||||
Record<Exclude<TextId, keyof PlaceholderOverrides>, []> {}
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
export const TEXTS: Readonly<Record<Language, Texts>> = {
|
||||
[Language.english]: ENGLISH,
|
||||
}
|
65
app/ide-desktop/lib/common/src/utilities/data/array.ts
Normal file
65
app/ide-desktop/lib/common/src/utilities/data/array.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/** @file Utilities for manipulating arrays. */
|
||||
|
||||
// ====================
|
||||
// === shallowEqual ===
|
||||
// ====================
|
||||
|
||||
/** Whether both arrays contain the same items. Does not recurse into the items. */
|
||||
export function shallowEqual<T>(a: readonly T[], b: readonly T[]) {
|
||||
return a.length === b.length && a.every((item, i) => item === b[i])
|
||||
}
|
||||
|
||||
// ================
|
||||
// === includes ===
|
||||
// ================
|
||||
|
||||
/** Returns a type predicate that returns true if and only if the value is in the array.
|
||||
* The array MUST contain every element of `T`. */
|
||||
export function includes<T>(array: T[], item: unknown): item is T {
|
||||
const arrayOfUnknown: unknown[] = array
|
||||
return arrayOfUnknown.includes(item)
|
||||
}
|
||||
|
||||
/** Returns a type predicate that returns true if and only if the value is in the iterable.
|
||||
* The iterable MUST contain every element of `T`. */
|
||||
export function includesPredicate<T>(array: Iterable<T>) {
|
||||
const set: Set<unknown> = array instanceof Set ? array : new Set<T>(array)
|
||||
return (item: unknown): item is T => set.has(item)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// === splice helpers ===
|
||||
// ======================
|
||||
|
||||
/** The value returned when {@link Array.findIndex} fails. */
|
||||
const NOT_FOUND = -1
|
||||
|
||||
/** Insert items before the first index `i` for which `predicate(array[i])` is `true`.
|
||||
* Insert the items at the end if the `predicate` never returns `true`. */
|
||||
export function spliceBefore<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
const index = array.findIndex(predicate)
|
||||
array.splice(index === NOT_FOUND ? array.length : index, 0, ...items)
|
||||
return array
|
||||
}
|
||||
|
||||
/** Return a copy of the array, with items inserted before the first index `i` for which
|
||||
* `predicate(array[i])` is `true`. The items are inserted at the end if the `predicate` never
|
||||
* returns `true`. */
|
||||
export function splicedBefore<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
return spliceBefore(Array.from(array), items, predicate)
|
||||
}
|
||||
|
||||
/** Insert items after the first index `i` for which `predicate(array[i])` is `true`.
|
||||
* Insert the items at the end if the `predicate` never returns `true`. */
|
||||
export function spliceAfter<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
const index = array.findIndex(predicate)
|
||||
array.splice(index === NOT_FOUND ? array.length : index + 1, 0, ...items)
|
||||
return array
|
||||
}
|
||||
|
||||
/** Return a copy of the array, with items inserted after the first index `i` for which
|
||||
* `predicate(array[i])` is `true`. The items are inserted at the end if the `predicate` never
|
||||
* returns `true`. */
|
||||
export function splicedAfter<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
return spliceAfter(Array.from(array), items, predicate)
|
||||
}
|
78
app/ide-desktop/lib/common/src/utilities/data/dateTime.ts
Normal file
78
app/ide-desktop/lib/common/src/utilities/data/dateTime.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/** @file Utilities for manipulating and displaying dates and times. */
|
||||
import * as newtype from './newtype'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The number of hours in half a day. This is used to get the number of hours for AM/PM time. */
|
||||
const HALF_DAY_HOURS = 12
|
||||
|
||||
/** A mapping from the month index returned by {@link Date.getMonth} to its full name. */
|
||||
export const MONTH_NAMES = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
]
|
||||
|
||||
// ================
|
||||
// === DateTime ===
|
||||
// ================
|
||||
|
||||
/** A string with date and time, following the RFC3339 specification. */
|
||||
export type Rfc3339DateTime = newtype.Newtype<string, 'Rfc3339DateTime'>
|
||||
/** Create a {@link Rfc3339DateTime}. */
|
||||
// This is a constructor function that constructs values of the type it is named after.
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const Rfc3339DateTime = newtype.newtypeConstructor<Rfc3339DateTime>()
|
||||
|
||||
/** Return a new {@link Date} with units below days (hours, minutes, seconds and milliseconds)
|
||||
* set to `0`. */
|
||||
export function toDate(dateTime: Date) {
|
||||
return new Date(dateTime.getFullYear(), dateTime.getMonth(), dateTime.getDate())
|
||||
}
|
||||
|
||||
/** Format a {@link Date} into the preferred date format: `YYYY-MM-DD`. */
|
||||
export function formatDate(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const dayOfMonth = date.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${dayOfMonth}`
|
||||
}
|
||||
|
||||
/** Format a {@link Date} into the preferred date-time format: `YYYY-MM-DD, hh:mm`. */
|
||||
export function formatDateTime(date: Date) {
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${formatDate(date)}, ${hour}:${minute}`
|
||||
}
|
||||
|
||||
/** Format a {@link Date} into the preferred chat-frienly format: `DD/MM/YYYY, hh:mm PM`. */
|
||||
export function formatDateTimeChatFriendly(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const dayOfMonth = date.getDate().toString().padStart(2, '0')
|
||||
let hourRaw = date.getHours()
|
||||
let amOrPm = 'AM'
|
||||
if (hourRaw > HALF_DAY_HOURS) {
|
||||
hourRaw -= HALF_DAY_HOURS
|
||||
amOrPm = 'PM'
|
||||
}
|
||||
const hour = hourRaw.toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${dayOfMonth}/${month}/${year} ${hour}:${minute} ${amOrPm}`
|
||||
}
|
||||
|
||||
/** Format a {@link Date} as a {@link Rfc3339DateTime}. */
|
||||
export function toRfc3339(date: Date) {
|
||||
return Rfc3339DateTime(date.toISOString())
|
||||
}
|
64
app/ide-desktop/lib/common/src/utilities/data/newtype.ts
Normal file
64
app/ide-desktop/lib/common/src/utilities/data/newtype.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/** @file Emulates `newtype`s in TypeScript. */
|
||||
|
||||
// ===============
|
||||
// === Newtype ===
|
||||
// ===============
|
||||
|
||||
/** An interface specifying the variant of a newtype. */
|
||||
export interface NewtypeVariant<TypeName extends string> {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
readonly _$type: TypeName
|
||||
}
|
||||
|
||||
/** An interface specifying the variant of a newtype, where the discriminator is mutable.
|
||||
* This is safe, as the discriminator should be a string literal type anyway. */
|
||||
// This is required for compatibility with the dependency `enso-chat`.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export interface MutableNewtypeVariant<TypeName extends string> {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_$type: TypeName
|
||||
}
|
||||
|
||||
/** Used to create a "branded type",
|
||||
* which contains a property that only exists at compile time.
|
||||
*
|
||||
* `Newtype<string, 'A'>` and `Newtype<string, 'B'>` are not compatible with each other,
|
||||
* however both are regular `string`s at runtime.
|
||||
*
|
||||
* This is useful in parameters that require values from a certain source,
|
||||
* for example IDs for a specific object type.
|
||||
*
|
||||
* It is similar to a `newtype` in other languages.
|
||||
* Note however because TypeScript is structurally typed,
|
||||
* a branded type is assignable to its base type:
|
||||
* `a: string = asNewtype<Newtype<string, 'Name'>>(b)` successfully typechecks. */
|
||||
export type Newtype<T, TypeName extends string> = NewtypeVariant<TypeName> & T
|
||||
|
||||
/** Extracts the original type out of a {@link Newtype}.
|
||||
* Its only use is in {@link newtypeConstructor}. */
|
||||
export type UnNewtype<T extends Newtype<unknown, string>> = T extends infer U &
|
||||
NewtypeVariant<T['_$type']>
|
||||
? U extends infer V & MutableNewtypeVariant<T['_$type']>
|
||||
? V
|
||||
: U
|
||||
: NotNewtype & Omit<T, '_$type'>
|
||||
|
||||
/** An interface that matches a type if and only if it is not a newtype. */
|
||||
export interface NotNewtype {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
readonly _$type?: never
|
||||
}
|
||||
|
||||
/** Converts a value that is not a newtype, to a value that is a newtype.
|
||||
* This function intentionally returns another function, to ensure that each function instance
|
||||
* is only used for one type, avoiding the de-optimization caused by polymorphic functions. */
|
||||
export function newtypeConstructor<T extends Newtype<unknown, string>>() {
|
||||
// This cast is unsafe.
|
||||
// `T` has an extra property `_$type` which is used purely for typechecking
|
||||
// and does not exist at runtime.
|
||||
//
|
||||
// The property name is specifically chosen to trigger eslint's `naming-convention` lint,
|
||||
// so it should not be possible to accidentally create a value with such a type.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return (s: NotNewtype & UnNewtype<T>) => s as unknown as T
|
||||
}
|
248
app/ide-desktop/lib/common/src/utilities/permissions.ts
Normal file
248
app/ide-desktop/lib/common/src/utilities/permissions.ts
Normal file
@ -0,0 +1,248 @@
|
||||
/** @file Utilities for working with permissions. */
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import type * as backend from '../services/Backend'
|
||||
|
||||
// ========================
|
||||
// === PermissionAction ===
|
||||
// ========================
|
||||
|
||||
/** Backend representation of user permission types. */
|
||||
export enum PermissionAction {
|
||||
own = 'Own',
|
||||
admin = 'Admin',
|
||||
edit = 'Edit',
|
||||
read = 'Read',
|
||||
readAndDocs = 'Read_docs',
|
||||
readAndExec = 'Read_exec',
|
||||
view = 'View',
|
||||
viewAndDocs = 'View_docs',
|
||||
viewAndExec = 'View_exec',
|
||||
}
|
||||
|
||||
/** Whether each {@link PermissionAction} can execute a project. */
|
||||
export const PERMISSION_ACTION_CAN_EXECUTE: Readonly<Record<PermissionAction, boolean>> = {
|
||||
[PermissionAction.own]: true,
|
||||
[PermissionAction.admin]: true,
|
||||
[PermissionAction.edit]: true,
|
||||
[PermissionAction.read]: false,
|
||||
[PermissionAction.readAndDocs]: false,
|
||||
[PermissionAction.readAndExec]: true,
|
||||
[PermissionAction.view]: false,
|
||||
[PermissionAction.viewAndDocs]: false,
|
||||
[PermissionAction.viewAndExec]: true,
|
||||
}
|
||||
|
||||
// ==================
|
||||
// === Permission ===
|
||||
// ==================
|
||||
|
||||
/** Type of permission. This determines what kind of border is displayed. */
|
||||
export enum Permission {
|
||||
owner = 'owner',
|
||||
admin = 'admin',
|
||||
edit = 'edit',
|
||||
read = 'read',
|
||||
view = 'view',
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
/** CSS classes for each permission. */
|
||||
export const PERMISSION_CLASS_NAME: Readonly<Record<Permission, string>> = {
|
||||
[Permission.owner]: 'text-tag-text bg-permission-owner',
|
||||
[Permission.admin]: 'text-tag-text bg-permission-admin',
|
||||
[Permission.edit]: 'text-tag-text bg-permission-edit',
|
||||
[Permission.read]: 'text-tag-text bg-permission-read',
|
||||
[Permission.view]: 'text-tag-text-2 bg-permission-view',
|
||||
[Permission.delete]: 'text-tag-text bg-delete',
|
||||
}
|
||||
|
||||
/** Precedences for each permission. A lower number means a higher priority. */
|
||||
export const PERMISSION_PRECEDENCE: Readonly<Record<Permission, number>> = {
|
||||
// These are not magic numbers - they are just a sequence of numbers.
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
[Permission.owner]: 0,
|
||||
[Permission.admin]: 1,
|
||||
[Permission.edit]: 2,
|
||||
[Permission.read]: 3,
|
||||
[Permission.view]: 4,
|
||||
[Permission.delete]: 1000,
|
||||
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
||||
}
|
||||
|
||||
/** Precedences for each permission action. A lower number means a higher priority. */
|
||||
export const PERMISSION_ACTION_PRECEDENCE: Readonly<Record<PermissionAction, number>> = {
|
||||
// These are not magic numbers - they are just a sequence of numbers.
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
[PermissionAction.own]: 0,
|
||||
[PermissionAction.admin]: 1,
|
||||
[PermissionAction.edit]: 2,
|
||||
[PermissionAction.read]: 3,
|
||||
[PermissionAction.readAndDocs]: 4,
|
||||
[PermissionAction.readAndExec]: 5,
|
||||
[PermissionAction.view]: 6,
|
||||
[PermissionAction.viewAndDocs]: 7,
|
||||
[PermissionAction.viewAndExec]: 8,
|
||||
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
||||
}
|
||||
|
||||
/** CSS classes for the docs permission. */
|
||||
export const DOCS_CLASS_NAME = 'text-tag-text bg-permission-docs'
|
||||
/** CSS classes for the execute permission. */
|
||||
export const EXEC_CLASS_NAME = 'text-tag-text bg-permission-exec'
|
||||
|
||||
/** The corresponding {@link Permissions} for each {@link PermissionAction}. */
|
||||
export const FROM_PERMISSION_ACTION: Readonly<Record<PermissionAction, Permissions>> = {
|
||||
[PermissionAction.own]: { type: Permission.owner },
|
||||
[PermissionAction.admin]: { type: Permission.admin },
|
||||
[PermissionAction.edit]: { type: Permission.edit },
|
||||
[PermissionAction.read]: {
|
||||
type: Permission.read,
|
||||
execute: false,
|
||||
docs: false,
|
||||
},
|
||||
[PermissionAction.readAndDocs]: {
|
||||
type: Permission.read,
|
||||
execute: false,
|
||||
docs: true,
|
||||
},
|
||||
[PermissionAction.readAndExec]: {
|
||||
type: Permission.read,
|
||||
execute: true,
|
||||
docs: false,
|
||||
},
|
||||
[PermissionAction.view]: {
|
||||
type: Permission.view,
|
||||
execute: false,
|
||||
docs: false,
|
||||
},
|
||||
[PermissionAction.viewAndDocs]: {
|
||||
type: Permission.view,
|
||||
execute: false,
|
||||
docs: true,
|
||||
},
|
||||
[PermissionAction.viewAndExec]: {
|
||||
type: Permission.view,
|
||||
execute: true,
|
||||
docs: false,
|
||||
},
|
||||
}
|
||||
|
||||
/** The corresponding {@link PermissionAction} for each {@link Permission}.
|
||||
* Assumes no docs sub-permission and no execute sub-permission. */
|
||||
export const TYPE_TO_PERMISSION_ACTION: Readonly<Record<Permission, PermissionAction>> = {
|
||||
[Permission.owner]: PermissionAction.own,
|
||||
[Permission.admin]: PermissionAction.admin,
|
||||
[Permission.edit]: PermissionAction.edit,
|
||||
[Permission.read]: PermissionAction.read,
|
||||
[Permission.view]: PermissionAction.view,
|
||||
// Should never happen, but provide a fallback just in case.
|
||||
[Permission.delete]: PermissionAction.view,
|
||||
}
|
||||
|
||||
/** The corresponding {@link text.TextId} for each {@link Permission}.
|
||||
* Assumes no docs sub-permission and no execute sub-permission. */
|
||||
export const TYPE_TO_TEXT_ID: Readonly<Record<Permission, text.TextId>> = {
|
||||
[Permission.owner]: 'ownerPermissionType',
|
||||
[Permission.admin]: 'adminPermissionType',
|
||||
[Permission.edit]: 'editPermissionType',
|
||||
[Permission.read]: 'readPermissionType',
|
||||
[Permission.view]: 'viewPermissionType',
|
||||
[Permission.delete]: 'deletePermissionType',
|
||||
} satisfies { [P in Permission]: `${P}PermissionType` }
|
||||
|
||||
/** The equivalent backend `PermissionAction` for a `Permissions`. */
|
||||
export function toPermissionAction(permissions: Permissions): PermissionAction {
|
||||
switch (permissions.type) {
|
||||
case Permission.owner: {
|
||||
return PermissionAction.own
|
||||
}
|
||||
case Permission.admin: {
|
||||
return PermissionAction.admin
|
||||
}
|
||||
case Permission.edit: {
|
||||
return PermissionAction.edit
|
||||
}
|
||||
case Permission.read: {
|
||||
return permissions.execute
|
||||
? permissions.docs
|
||||
? /* should never happen, but use a fallback value */
|
||||
PermissionAction.readAndExec
|
||||
: PermissionAction.readAndExec
|
||||
: permissions.docs
|
||||
? PermissionAction.readAndDocs
|
||||
: PermissionAction.read
|
||||
}
|
||||
case Permission.view: {
|
||||
return permissions.execute
|
||||
? permissions.docs
|
||||
? /* should never happen, but use a fallback value */
|
||||
PermissionAction.viewAndExec
|
||||
: PermissionAction.viewAndExec
|
||||
: permissions.docs
|
||||
? PermissionAction.viewAndDocs
|
||||
: PermissionAction.view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================
|
||||
// === Permissions ===
|
||||
// ===================
|
||||
|
||||
/** Properties common to all permissions. */
|
||||
interface BasePermissions<T extends Permission> {
|
||||
readonly type: T
|
||||
}
|
||||
|
||||
/** Owner permissions for an asset. */
|
||||
interface OwnerPermissions extends BasePermissions<Permission.owner> {}
|
||||
|
||||
/** Admin permissions for an asset. */
|
||||
interface AdminPermissions extends BasePermissions<Permission.admin> {}
|
||||
|
||||
/** Editor permissions for an asset. */
|
||||
interface EditPermissions extends BasePermissions<Permission.edit> {}
|
||||
|
||||
/** Reader permissions for an asset. */
|
||||
interface ReadPermissions extends BasePermissions<Permission.read> {
|
||||
readonly docs: boolean
|
||||
readonly execute: boolean
|
||||
}
|
||||
|
||||
/** Viewer permissions for an asset. */
|
||||
interface ViewPermissions extends BasePermissions<Permission.view> {
|
||||
readonly docs: boolean
|
||||
readonly execute: boolean
|
||||
}
|
||||
|
||||
/** Detailed permission information. This is used to draw the border. */
|
||||
export type Permissions =
|
||||
| AdminPermissions
|
||||
| EditPermissions
|
||||
| OwnerPermissions
|
||||
| ReadPermissions
|
||||
| ViewPermissions
|
||||
|
||||
export const DEFAULT_PERMISSIONS: Permissions = Object.freeze({
|
||||
type: Permission.view,
|
||||
docs: false,
|
||||
execute: false,
|
||||
})
|
||||
|
||||
// ======================================
|
||||
// === tryGetSingletonOwnerPermission ===
|
||||
// ======================================
|
||||
|
||||
/** Return an array containing the owner permission if `owner` is not `null`,
|
||||
* else return an empty array (`[]`). */
|
||||
export function tryGetSingletonOwnerPermission(
|
||||
owner: backend.User | null
|
||||
): backend.UserPermission[] {
|
||||
if (owner != null) {
|
||||
const { organizationId, userId, name, email } = owner
|
||||
return [{ user: { organizationId, userId, name, email }, permission: PermissionAction.own }]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
14
app/ide-desktop/lib/common/src/utilities/uniqueString.ts
Normal file
14
app/ide-desktop/lib/common/src/utilities/uniqueString.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/** @file A function that generates a unique string. */
|
||||
|
||||
// ====================
|
||||
// === uniqueString ===
|
||||
// ====================
|
||||
|
||||
// This is initialized to an unusual number, to minimize the chances of collision.
|
||||
let counter = Number(new Date()) >>> 2
|
||||
|
||||
/** Returns a new, mostly unique string. */
|
||||
export function uniqueString(): string {
|
||||
counter += 1
|
||||
return counter.toString()
|
||||
}
|
@ -5,5 +5,5 @@
|
||||
"checkJs": false,
|
||||
"skipLibCheck": false
|
||||
},
|
||||
"include": ["./src/", "../types/"]
|
||||
"include": ["./src/", "./src/text/english.json", "../types/"]
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ import * as React from 'react'
|
||||
|
||||
import BlankIcon from 'enso-assets/blank.svg'
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import type * as inputBindings from '#/configurations/inputBindings'
|
||||
|
||||
|
@ -8,8 +8,7 @@ import * as React from 'react'
|
||||
import * as tw from 'tailwind-merge'
|
||||
|
||||
import Check from 'enso-assets/check_mark.svg'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
|
@ -7,8 +7,7 @@ import OptionKeyIcon from 'enso-assets/option_key.svg'
|
||||
import ShiftKeyIcon from 'enso-assets/shift_key.svg'
|
||||
import WindowsKeyIcon from 'enso-assets/windows_key.svg'
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import type * as dashboardInputBindings from '#/configurations/inputBindings'
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/** @file Permissions for a specific user or user group on a specific asset. */
|
||||
import * as React from 'react'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as backendHooks from '#/hooks/backendHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
@ -6,8 +6,7 @@ import DocsIcon from 'enso-assets/docs.svg'
|
||||
import PeopleIcon from 'enso-assets/people.svg'
|
||||
import TagIcon from 'enso-assets/tag.svg'
|
||||
import TimeIcon from 'enso-assets/time.svg'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as backend from '#/services/Backend'
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
* Paywall configuration for different plans.
|
||||
*/
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as backend from '#/services/Backend'
|
||||
|
||||
|
@ -3,7 +3,7 @@ import * as React from 'react'
|
||||
|
||||
import * as toastify from 'react-toastify'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as loggerProvider from '#/providers/LoggerProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
@ -5,8 +5,7 @@ import CloudIcon from 'enso-assets/cloud.svg'
|
||||
import ComputerIcon from 'enso-assets/computer.svg'
|
||||
import RecentIcon from 'enso-assets/recent.svg'
|
||||
import Trash2Icon from 'enso-assets/trash2.svg'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as mimeTypes from '#/data/mimeTypes'
|
||||
|
||||
|
@ -9,8 +9,7 @@ import LogIcon from 'enso-assets/log.svg'
|
||||
import PeopleSettingsIcon from 'enso-assets/people_settings.svg'
|
||||
import PeopleIcon from 'enso-assets/people.svg'
|
||||
import SettingsIcon from 'enso-assets/settings.svg'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as inputBindings from '#/configurations/inputBindings'
|
||||
|
||||
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
||||
import * as reactQuery from '@tanstack/react-query'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
|
@ -2,8 +2,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import LogoIcon from 'enso-assets/enso_logo.svg'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
@ -6,8 +6,7 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import Check from 'enso-assets/check_mark.svg'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import OpenInNewTabIcon from 'enso-assets/open.svg'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as appUtils from '#/appUtils'
|
||||
import type * as text from '#/text'
|
||||
|
||||
import * as textProvider from '#/providers/TextProvider'
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file Constants for the subscribe page.
|
||||
*/
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import * as backendModule from '#/services/Backend'
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
* React context. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as text from '#/text'
|
||||
import * as text from 'enso-common/src/text'
|
||||
|
||||
import * as object from '#/utilities/object'
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,7 @@
|
||||
* an API endpoint. The functions are asynchronous and return a {@link Promise} that resolves to
|
||||
* the response from the API. */
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import type * as text from '#/text'
|
||||
import type * as text from 'enso-common/src/text'
|
||||
|
||||
import type * as loggerProvider from '#/providers/LoggerProvider'
|
||||
import type * as textProvider from '#/providers/TextProvider'
|
||||
|
@ -1,136 +0,0 @@
|
||||
/** @file Functions related to displaying text. */
|
||||
|
||||
import ENGLISH from '#/text/english.json' with { type: 'json' }
|
||||
|
||||
// =============
|
||||
// === Types ===
|
||||
// =============
|
||||
|
||||
/** Possible languages in which to display text. */
|
||||
export enum Language {
|
||||
english = 'english',
|
||||
}
|
||||
|
||||
export const LANGUAGE_TO_LOCALE: Record<Language, string> = {
|
||||
[Language.english]: 'en-US',
|
||||
}
|
||||
|
||||
/** An object containing the corresponding localized text for each text ID. */
|
||||
type Texts = typeof ENGLISH
|
||||
/** All possible text IDs. */
|
||||
export type TextId = keyof Texts
|
||||
|
||||
/** Overrides the default number of placeholders (0). */
|
||||
interface PlaceholderOverrides {
|
||||
readonly copyAssetError: [assetName: string]
|
||||
readonly moveAssetError: [assetName: string]
|
||||
readonly findProjectError: [projectName: string]
|
||||
readonly openProjectError: [projectName: string]
|
||||
readonly deleteAssetError: [assetName: string]
|
||||
readonly restoreAssetError: [assetName: string]
|
||||
readonly restoreProjectError: [projectName: string]
|
||||
readonly unknownThreadIdError: [threadId: string]
|
||||
readonly needsOwnerError: [assetType: string]
|
||||
readonly inviteSuccess: [userEmail: string]
|
||||
readonly inviteManyUsersSuccess: [userCount: number]
|
||||
|
||||
readonly deleteLabelActionText: [labelName: string]
|
||||
readonly deleteSelectedAssetActionText: [assetName: string]
|
||||
readonly deleteSelectedAssetsActionText: [count: number]
|
||||
readonly deleteSelectedAssetForeverActionText: [assetName: string]
|
||||
readonly deleteSelectedAssetsForeverActionText: [count: number]
|
||||
readonly deleteUserActionText: [userName: string]
|
||||
readonly deleteUserGroupActionText: [groupName: string]
|
||||
readonly removeUserFromUserGroupActionText: [userName: string, groupName: string]
|
||||
readonly confirmPrompt: [action: string]
|
||||
readonly deleteTheAssetTypeTitle: [assetType: string, assetName: string]
|
||||
readonly couldNotInviteUser: [userEmail: string]
|
||||
readonly filesWithoutConflicts: [fileCount: number]
|
||||
readonly projectsWithoutConflicts: [projectCount: number]
|
||||
readonly andOtherFiles: [fileCount: number]
|
||||
readonly andOtherProjects: [projectCount: number]
|
||||
readonly emailIsNotAValidEmail: [userEmail: string]
|
||||
readonly userIsAlreadyInTheOrganization: [userEmail: string]
|
||||
readonly youAreAlreadyAddingUser: [userEmail: string]
|
||||
readonly lastModifiedOn: [dateString: string]
|
||||
readonly versionX: [version: number | string]
|
||||
readonly buildX: [build: string]
|
||||
readonly electronVersionX: [electronVersion: string]
|
||||
readonly chromeVersionX: [chromeVersion: string]
|
||||
readonly userAgentX: [userAgent: string]
|
||||
readonly compareVersionXWithLatest: [versionNumber: number]
|
||||
readonly onDateX: [dateString: string]
|
||||
readonly xUsersAndGroupsSelected: [usersAndGroupsCount: number]
|
||||
readonly upgradeTo: [planName: string]
|
||||
readonly enterTheNewKeyboardShortcutFor: [actionName: string]
|
||||
readonly downloadProjectError: [projectName: string]
|
||||
readonly downloadFileError: [fileName: string]
|
||||
readonly downloadDatalinkError: [datalinkName: string]
|
||||
readonly deleteUserGroupError: [userGroupName: string]
|
||||
readonly removeUserFromUserGroupError: [userName: string, userGroupName: string]
|
||||
readonly deleteUserError: [userName: string]
|
||||
|
||||
readonly inviteUserBackendError: [string]
|
||||
readonly changeUserGroupsBackendError: [string]
|
||||
readonly listFolderBackendError: [string]
|
||||
readonly createFolderBackendError: [string]
|
||||
readonly updateFolderBackendError: [string]
|
||||
readonly listAssetVersionsBackendError: [string]
|
||||
readonly getFileContentsBackendError: [string]
|
||||
readonly updateAssetBackendError: [string]
|
||||
readonly deleteAssetBackendError: [string]
|
||||
readonly undoDeleteAssetBackendError: [string]
|
||||
readonly copyAssetBackendError: [string, string]
|
||||
readonly createProjectBackendError: [string]
|
||||
readonly restoreProjectBackendError: [string]
|
||||
readonly duplicateProjectBackendError: [string]
|
||||
readonly closeProjectBackendError: [string]
|
||||
readonly listProjectSessionsBackendError: [string]
|
||||
readonly getProjectDetailsBackendError: [string]
|
||||
readonly getProjectLogsBackendError: [string]
|
||||
readonly openProjectBackendError: [string]
|
||||
readonly openProjectMissingCredentialsBackendError: [string]
|
||||
readonly updateProjectBackendError: [string]
|
||||
readonly checkResourcesBackendError: [string]
|
||||
readonly uploadFileWithNameBackendError: [string]
|
||||
readonly getFileDetailsBackendError: [string]
|
||||
readonly createDatalinkBackendError: [string]
|
||||
readonly getDatalinkBackendError: [string]
|
||||
readonly deleteDatalinkBackendError: [string]
|
||||
readonly createSecretBackendError: [string]
|
||||
readonly getSecretBackendError: [string]
|
||||
readonly updateSecretBackendError: [string]
|
||||
readonly createLabelBackendError: [string]
|
||||
readonly associateLabelsBackendError: [string]
|
||||
readonly deleteLabelBackendError: [string]
|
||||
readonly createUserGroupBackendError: [string]
|
||||
readonly deleteUserGroupBackendError: [string]
|
||||
readonly listVersionsBackendError: [string]
|
||||
readonly createCheckoutSessionBackendError: [string]
|
||||
readonly getCheckoutSessionBackendError: [string]
|
||||
readonly getDefaultVersionBackendError: [string]
|
||||
readonly logEventBackendError: [string]
|
||||
|
||||
readonly subscribeSuccessSubtitle: [string]
|
||||
readonly assetsDropFilesDescription: [count: number]
|
||||
|
||||
readonly paywallAvailabilityLevel: [plan: string]
|
||||
readonly paywallScreenDescription: [plan: string]
|
||||
readonly userGroupsLimitMessage: [limit: number]
|
||||
readonly inviteFormSeatsLeftError: [exceedBy: number]
|
||||
readonly inviteFormSeatsLeft: [seatsLeft: number]
|
||||
readonly seatsLeft: [seatsLeft: number, seatsTotal: number]
|
||||
}
|
||||
|
||||
/** An tuple of `string` for placeholders for each {@link TextId}. */
|
||||
export interface Replacements
|
||||
extends PlaceholderOverrides,
|
||||
Record<Exclude<TextId, keyof PlaceholderOverrides>, []> {}
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
export const TEXTS: Readonly<Record<Language, Texts>> = {
|
||||
[Language.english]: ENGLISH,
|
||||
}
|
@ -1,65 +1,3 @@
|
||||
/** @file Utilities for manipulating arrays. */
|
||||
|
||||
// ====================
|
||||
// === shallowEqual ===
|
||||
// ====================
|
||||
|
||||
/** Whether both arrays contain the same items. Does not recurse into the items. */
|
||||
export function shallowEqual<T>(a: readonly T[], b: readonly T[]) {
|
||||
return a.length === b.length && a.every((item, i) => item === b[i])
|
||||
}
|
||||
|
||||
// ================
|
||||
// === includes ===
|
||||
// ================
|
||||
|
||||
/** Returns a type predicate that returns true if and only if the value is in the array.
|
||||
* The array MUST contain every element of `T`. */
|
||||
export function includes<T>(array: T[], item: unknown): item is T {
|
||||
const arrayOfUnknown: unknown[] = array
|
||||
return arrayOfUnknown.includes(item)
|
||||
}
|
||||
|
||||
/** Returns a type predicate that returns true if and only if the value is in the iterable.
|
||||
* The iterable MUST contain every element of `T`. */
|
||||
export function includesPredicate<T>(array: Iterable<T>) {
|
||||
const set: Set<unknown> = array instanceof Set ? array : new Set<T>(array)
|
||||
return (item: unknown): item is T => set.has(item)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// === splice helpers ===
|
||||
// ======================
|
||||
|
||||
/** The value returned when {@link Array.findIndex} fails. */
|
||||
const NOT_FOUND = -1
|
||||
|
||||
/** Insert items before the first index `i` for which `predicate(array[i])` is `true`.
|
||||
* Insert the items at the end if the `predicate` never returns `true`. */
|
||||
export function spliceBefore<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
const index = array.findIndex(predicate)
|
||||
array.splice(index === NOT_FOUND ? array.length : index, 0, ...items)
|
||||
return array
|
||||
}
|
||||
|
||||
/** Return a copy of the array, with items inserted before the first index `i` for which
|
||||
* `predicate(array[i])` is `true`. The items are inserted at the end if the `predicate` never
|
||||
* returns `true`. */
|
||||
export function splicedBefore<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
return spliceBefore(Array.from(array), items, predicate)
|
||||
}
|
||||
|
||||
/** Insert items after the first index `i` for which `predicate(array[i])` is `true`.
|
||||
* Insert the items at the end if the `predicate` never returns `true`. */
|
||||
export function spliceAfter<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
const index = array.findIndex(predicate)
|
||||
array.splice(index === NOT_FOUND ? array.length : index + 1, 0, ...items)
|
||||
return array
|
||||
}
|
||||
|
||||
/** Return a copy of the array, with items inserted after the first index `i` for which
|
||||
* `predicate(array[i])` is `true`. The items are inserted at the end if the `predicate` never
|
||||
* returns `true`. */
|
||||
export function splicedAfter<T>(array: T[], items: T[], predicate: (value: T) => boolean) {
|
||||
return spliceAfter(Array.from(array), items, predicate)
|
||||
}
|
||||
export * from 'enso-common/src/utilities/data/array'
|
||||
|
@ -1,78 +1,3 @@
|
||||
/** @file Utilities for manipulating and displaying dates and times. */
|
||||
import * as newtype from '#/utilities/newtype'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/** The number of hours in half a day. This is used to get the number of hours for AM/PM time. */
|
||||
const HALF_DAY_HOURS = 12
|
||||
|
||||
/** A mapping from the month index returned by {@link Date.getMonth} to its full name. */
|
||||
export const MONTH_NAMES = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
]
|
||||
|
||||
// ================
|
||||
// === DateTime ===
|
||||
// ================
|
||||
|
||||
/** A string with date and time, following the RFC3339 specification. */
|
||||
export type Rfc3339DateTime = newtype.Newtype<string, 'Rfc3339DateTime'>
|
||||
/** Create a {@link Rfc3339DateTime}. */
|
||||
// This is a constructor function that constructs values of the type it is named after.
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const Rfc3339DateTime = newtype.newtypeConstructor<Rfc3339DateTime>()
|
||||
|
||||
/** Return a new {@link Date} with units below days (hours, minutes, seconds and milliseconds)
|
||||
* set to `0`. */
|
||||
export function toDate(dateTime: Date) {
|
||||
return new Date(dateTime.getFullYear(), dateTime.getMonth(), dateTime.getDate())
|
||||
}
|
||||
|
||||
/** Format a {@link Date} into the preferred date format: `YYYY-MM-DD`. */
|
||||
export function formatDate(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const dayOfMonth = date.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${dayOfMonth}`
|
||||
}
|
||||
|
||||
/** Format a {@link Date} into the preferred date-time format: `YYYY-MM-DD, hh:mm`. */
|
||||
export function formatDateTime(date: Date) {
|
||||
const hour = date.getHours().toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${formatDate(date)}, ${hour}:${minute}`
|
||||
}
|
||||
|
||||
/** Format a {@link Date} into the preferred chat-frienly format: `DD/MM/YYYY, hh:mm PM`. */
|
||||
export function formatDateTimeChatFriendly(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const dayOfMonth = date.getDate().toString().padStart(2, '0')
|
||||
let hourRaw = date.getHours()
|
||||
let amOrPm = 'AM'
|
||||
if (hourRaw > HALF_DAY_HOURS) {
|
||||
hourRaw -= HALF_DAY_HOURS
|
||||
amOrPm = 'PM'
|
||||
}
|
||||
const hour = hourRaw.toString().padStart(2, '0')
|
||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${dayOfMonth}/${month}/${year} ${hour}:${minute} ${amOrPm}`
|
||||
}
|
||||
|
||||
/** Format a {@link Date} as a {@link Rfc3339DateTime}. */
|
||||
export function toRfc3339(date: Date) {
|
||||
return Rfc3339DateTime(date.toISOString())
|
||||
}
|
||||
export * from 'enso-common/src/utilities/data/dateTime'
|
||||
|
@ -1,64 +1,3 @@
|
||||
/** @file Emulates `newtype`s in TypeScript. */
|
||||
|
||||
// ===============
|
||||
// === Newtype ===
|
||||
// ===============
|
||||
|
||||
/** An interface specifying the variant of a newtype. */
|
||||
export interface NewtypeVariant<TypeName extends string> {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
readonly _$type: TypeName
|
||||
}
|
||||
|
||||
/** An interface specifying the variant of a newtype, where the discriminator is mutable.
|
||||
* This is safe, as the discriminator should be a string literal type anyway. */
|
||||
// This is required for compatibility with the dependency `enso-chat`.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export interface MutableNewtypeVariant<TypeName extends string> {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_$type: TypeName
|
||||
}
|
||||
|
||||
/** Used to create a "branded type",
|
||||
* which contains a property that only exists at compile time.
|
||||
*
|
||||
* `Newtype<string, 'A'>` and `Newtype<string, 'B'>` are not compatible with each other,
|
||||
* however both are regular `string`s at runtime.
|
||||
*
|
||||
* This is useful in parameters that require values from a certain source,
|
||||
* for example IDs for a specific object type.
|
||||
*
|
||||
* It is similar to a `newtype` in other languages.
|
||||
* Note however because TypeScript is structurally typed,
|
||||
* a branded type is assignable to its base type:
|
||||
* `a: string = asNewtype<Newtype<string, 'Name'>>(b)` successfully typechecks. */
|
||||
export type Newtype<T, TypeName extends string> = NewtypeVariant<TypeName> & T
|
||||
|
||||
/** Extracts the original type out of a {@link Newtype}.
|
||||
* Its only use is in {@link newtypeConstructor}. */
|
||||
export type UnNewtype<T extends Newtype<unknown, string>> = T extends infer U &
|
||||
NewtypeVariant<T['_$type']>
|
||||
? U extends infer V & MutableNewtypeVariant<T['_$type']>
|
||||
? V
|
||||
: U
|
||||
: NotNewtype & Omit<T, '_$type'>
|
||||
|
||||
/** An interface that matches a type if and only if it is not a newtype. */
|
||||
export interface NotNewtype {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
readonly _$type?: never
|
||||
}
|
||||
|
||||
/** Converts a value that is not a newtype, to a value that is a newtype.
|
||||
* This function intentionally returns another function, to ensure that each function instance
|
||||
* is only used for one type, avoiding the de-optimization caused by polymorphic functions. */
|
||||
export function newtypeConstructor<T extends Newtype<unknown, string>>() {
|
||||
// This cast is unsafe.
|
||||
// `T` has an extra property `_$type` which is used purely for typechecking
|
||||
// and does not exist at runtime.
|
||||
//
|
||||
// The property name is specifically chosen to trigger eslint's `naming-convention` lint,
|
||||
// so it should not be possible to accidentally create a value with such a type.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return (s: NotNewtype & UnNewtype<T>) => s as unknown as T
|
||||
}
|
||||
export * from 'enso-common/src/utilities/data/newtype'
|
||||
|
@ -1,248 +1,3 @@
|
||||
/** @file Utilities for working with permissions. */
|
||||
import type * as text from '#/text'
|
||||
|
||||
import type * as backend from '#/services/Backend'
|
||||
|
||||
// ========================
|
||||
// === PermissionAction ===
|
||||
// ========================
|
||||
|
||||
/** Backend representation of user permission types. */
|
||||
export enum PermissionAction {
|
||||
own = 'Own',
|
||||
admin = 'Admin',
|
||||
edit = 'Edit',
|
||||
read = 'Read',
|
||||
readAndDocs = 'Read_docs',
|
||||
readAndExec = 'Read_exec',
|
||||
view = 'View',
|
||||
viewAndDocs = 'View_docs',
|
||||
viewAndExec = 'View_exec',
|
||||
}
|
||||
|
||||
/** Whether each {@link PermissionAction} can execute a project. */
|
||||
export const PERMISSION_ACTION_CAN_EXECUTE: Readonly<Record<PermissionAction, boolean>> = {
|
||||
[PermissionAction.own]: true,
|
||||
[PermissionAction.admin]: true,
|
||||
[PermissionAction.edit]: true,
|
||||
[PermissionAction.read]: false,
|
||||
[PermissionAction.readAndDocs]: false,
|
||||
[PermissionAction.readAndExec]: true,
|
||||
[PermissionAction.view]: false,
|
||||
[PermissionAction.viewAndDocs]: false,
|
||||
[PermissionAction.viewAndExec]: true,
|
||||
}
|
||||
|
||||
// ==================
|
||||
// === Permission ===
|
||||
// ==================
|
||||
|
||||
/** Type of permission. This determines what kind of border is displayed. */
|
||||
export enum Permission {
|
||||
owner = 'owner',
|
||||
admin = 'admin',
|
||||
edit = 'edit',
|
||||
read = 'read',
|
||||
view = 'view',
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
/** CSS classes for each permission. */
|
||||
export const PERMISSION_CLASS_NAME: Readonly<Record<Permission, string>> = {
|
||||
[Permission.owner]: 'text-tag-text bg-permission-owner',
|
||||
[Permission.admin]: 'text-tag-text bg-permission-admin',
|
||||
[Permission.edit]: 'text-tag-text bg-permission-edit',
|
||||
[Permission.read]: 'text-tag-text bg-permission-read',
|
||||
[Permission.view]: 'text-tag-text-2 bg-permission-view',
|
||||
[Permission.delete]: 'text-tag-text bg-delete',
|
||||
}
|
||||
|
||||
/** Precedences for each permission. A lower number means a higher priority. */
|
||||
export const PERMISSION_PRECEDENCE: Readonly<Record<Permission, number>> = {
|
||||
// These are not magic numbers - they are just a sequence of numbers.
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
[Permission.owner]: 0,
|
||||
[Permission.admin]: 1,
|
||||
[Permission.edit]: 2,
|
||||
[Permission.read]: 3,
|
||||
[Permission.view]: 4,
|
||||
[Permission.delete]: 1000,
|
||||
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
||||
}
|
||||
|
||||
/** Precedences for each permission action. A lower number means a higher priority. */
|
||||
export const PERMISSION_ACTION_PRECEDENCE: Readonly<Record<PermissionAction, number>> = {
|
||||
// These are not magic numbers - they are just a sequence of numbers.
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
[PermissionAction.own]: 0,
|
||||
[PermissionAction.admin]: 1,
|
||||
[PermissionAction.edit]: 2,
|
||||
[PermissionAction.read]: 3,
|
||||
[PermissionAction.readAndDocs]: 4,
|
||||
[PermissionAction.readAndExec]: 5,
|
||||
[PermissionAction.view]: 6,
|
||||
[PermissionAction.viewAndDocs]: 7,
|
||||
[PermissionAction.viewAndExec]: 8,
|
||||
/* eslint-enable @typescript-eslint/no-magic-numbers */
|
||||
}
|
||||
|
||||
/** CSS classes for the docs permission. */
|
||||
export const DOCS_CLASS_NAME = 'text-tag-text bg-permission-docs'
|
||||
/** CSS classes for the execute permission. */
|
||||
export const EXEC_CLASS_NAME = 'text-tag-text bg-permission-exec'
|
||||
|
||||
/** The corresponding {@link Permissions} for each {@link PermissionAction}. */
|
||||
export const FROM_PERMISSION_ACTION: Readonly<Record<PermissionAction, Permissions>> = {
|
||||
[PermissionAction.own]: { type: Permission.owner },
|
||||
[PermissionAction.admin]: { type: Permission.admin },
|
||||
[PermissionAction.edit]: { type: Permission.edit },
|
||||
[PermissionAction.read]: {
|
||||
type: Permission.read,
|
||||
execute: false,
|
||||
docs: false,
|
||||
},
|
||||
[PermissionAction.readAndDocs]: {
|
||||
type: Permission.read,
|
||||
execute: false,
|
||||
docs: true,
|
||||
},
|
||||
[PermissionAction.readAndExec]: {
|
||||
type: Permission.read,
|
||||
execute: true,
|
||||
docs: false,
|
||||
},
|
||||
[PermissionAction.view]: {
|
||||
type: Permission.view,
|
||||
execute: false,
|
||||
docs: false,
|
||||
},
|
||||
[PermissionAction.viewAndDocs]: {
|
||||
type: Permission.view,
|
||||
execute: false,
|
||||
docs: true,
|
||||
},
|
||||
[PermissionAction.viewAndExec]: {
|
||||
type: Permission.view,
|
||||
execute: true,
|
||||
docs: false,
|
||||
},
|
||||
}
|
||||
|
||||
/** The corresponding {@link PermissionAction} for each {@link Permission}.
|
||||
* Assumes no docs sub-permission and no execute sub-permission. */
|
||||
export const TYPE_TO_PERMISSION_ACTION: Readonly<Record<Permission, PermissionAction>> = {
|
||||
[Permission.owner]: PermissionAction.own,
|
||||
[Permission.admin]: PermissionAction.admin,
|
||||
[Permission.edit]: PermissionAction.edit,
|
||||
[Permission.read]: PermissionAction.read,
|
||||
[Permission.view]: PermissionAction.view,
|
||||
// Should never happen, but provide a fallback just in case.
|
||||
[Permission.delete]: PermissionAction.view,
|
||||
}
|
||||
|
||||
/** The corresponding {@link text.TextId} for each {@link Permission}.
|
||||
* Assumes no docs sub-permission and no execute sub-permission. */
|
||||
export const TYPE_TO_TEXT_ID: Readonly<Record<Permission, text.TextId>> = {
|
||||
[Permission.owner]: 'ownerPermissionType',
|
||||
[Permission.admin]: 'adminPermissionType',
|
||||
[Permission.edit]: 'editPermissionType',
|
||||
[Permission.read]: 'readPermissionType',
|
||||
[Permission.view]: 'viewPermissionType',
|
||||
[Permission.delete]: 'deletePermissionType',
|
||||
} satisfies { [P in Permission]: `${P}PermissionType` }
|
||||
|
||||
/** The equivalent backend `PermissionAction` for a `Permissions`. */
|
||||
export function toPermissionAction(permissions: Permissions): PermissionAction {
|
||||
switch (permissions.type) {
|
||||
case Permission.owner: {
|
||||
return PermissionAction.own
|
||||
}
|
||||
case Permission.admin: {
|
||||
return PermissionAction.admin
|
||||
}
|
||||
case Permission.edit: {
|
||||
return PermissionAction.edit
|
||||
}
|
||||
case Permission.read: {
|
||||
return permissions.execute
|
||||
? permissions.docs
|
||||
? /* should never happen, but use a fallback value */
|
||||
PermissionAction.readAndExec
|
||||
: PermissionAction.readAndExec
|
||||
: permissions.docs
|
||||
? PermissionAction.readAndDocs
|
||||
: PermissionAction.read
|
||||
}
|
||||
case Permission.view: {
|
||||
return permissions.execute
|
||||
? permissions.docs
|
||||
? /* should never happen, but use a fallback value */
|
||||
PermissionAction.viewAndExec
|
||||
: PermissionAction.viewAndExec
|
||||
: permissions.docs
|
||||
? PermissionAction.viewAndDocs
|
||||
: PermissionAction.view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================
|
||||
// === Permissions ===
|
||||
// ===================
|
||||
|
||||
/** Properties common to all permissions. */
|
||||
interface BasePermissions<T extends Permission> {
|
||||
readonly type: T
|
||||
}
|
||||
|
||||
/** Owner permissions for an asset. */
|
||||
interface OwnerPermissions extends BasePermissions<Permission.owner> {}
|
||||
|
||||
/** Admin permissions for an asset. */
|
||||
interface AdminPermissions extends BasePermissions<Permission.admin> {}
|
||||
|
||||
/** Editor permissions for an asset. */
|
||||
interface EditPermissions extends BasePermissions<Permission.edit> {}
|
||||
|
||||
/** Reader permissions for an asset. */
|
||||
interface ReadPermissions extends BasePermissions<Permission.read> {
|
||||
readonly docs: boolean
|
||||
readonly execute: boolean
|
||||
}
|
||||
|
||||
/** Viewer permissions for an asset. */
|
||||
interface ViewPermissions extends BasePermissions<Permission.view> {
|
||||
readonly docs: boolean
|
||||
readonly execute: boolean
|
||||
}
|
||||
|
||||
/** Detailed permission information. This is used to draw the border. */
|
||||
export type Permissions =
|
||||
| AdminPermissions
|
||||
| EditPermissions
|
||||
| OwnerPermissions
|
||||
| ReadPermissions
|
||||
| ViewPermissions
|
||||
|
||||
export const DEFAULT_PERMISSIONS: Permissions = Object.freeze({
|
||||
type: Permission.view,
|
||||
docs: false,
|
||||
execute: false,
|
||||
})
|
||||
|
||||
// ======================================
|
||||
// === tryGetSingletonOwnerPermission ===
|
||||
// ======================================
|
||||
|
||||
/** Return an array containing the owner permission if `owner` is not `null`,
|
||||
* else return an empty array (`[]`). */
|
||||
export function tryGetSingletonOwnerPermission(
|
||||
owner: backend.User | null
|
||||
): backend.UserPermission[] {
|
||||
if (owner != null) {
|
||||
const { organizationId, userId, name, email } = owner
|
||||
return [{ user: { organizationId, userId, name, email }, permission: PermissionAction.own }]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
export * from 'enso-common/src/utilities/permissions'
|
||||
|
@ -1,14 +1,3 @@
|
||||
/** @file A function that generates a unique string. */
|
||||
|
||||
// ====================
|
||||
// === uniqueString ===
|
||||
// ====================
|
||||
|
||||
// This is initialized to an unusual number, to minimize the chances of collision.
|
||||
let counter = Number(new Date()) >>> 2
|
||||
|
||||
/** Returns a new, mostly unique string. */
|
||||
export function uniqueString(): string {
|
||||
counter += 1
|
||||
return counter.toString()
|
||||
}
|
||||
export * from 'enso-common/src/utilities/uniqueString'
|
||||
|
Loading…
Reference in New Issue
Block a user