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:
Kaz Wesley 2024-07-10 14:04:37 -07:00 committed by GitHub
parent 60c1a0e1f6
commit 32e843c614
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 2110 additions and 2094 deletions

View File

@ -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",

File diff suppressed because it is too large Load Diff

View 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,
}

View 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)
}

View 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())
}

View 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
}

View 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 []
}
}

View 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()
}

View File

@ -5,5 +5,5 @@
"checkJs": false,
"skipLibCheck": false
},
"include": ["./src/", "../types/"]
"include": ["./src/", "./src/text/english.json", "../types/"]
}

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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,
}

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'