mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 16:18:23 +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/detect": "./src/detect.ts",
|
||||||
"./src/gtag": "./src/gtag.ts",
|
"./src/gtag": "./src/gtag.ts",
|
||||||
"./src/load": "./src/load.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": {
|
"peerDependencies": {
|
||||||
"@tanstack/query-core": "5.45.0",
|
"@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,
|
"checkJs": false,
|
||||||
"skipLibCheck": 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 BlankIcon from 'enso-assets/blank.svg'
|
||||||
import * as detect from 'enso-common/src/detect'
|
import * as detect from 'enso-common/src/detect'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import type * as inputBindings from '#/configurations/inputBindings'
|
import type * as inputBindings from '#/configurations/inputBindings'
|
||||||
|
|
||||||
|
@ -8,8 +8,7 @@ import * as React from 'react'
|
|||||||
import * as tw from 'tailwind-merge'
|
import * as tw from 'tailwind-merge'
|
||||||
|
|
||||||
import Check from 'enso-assets/check_mark.svg'
|
import Check from 'enso-assets/check_mark.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
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 ShiftKeyIcon from 'enso-assets/shift_key.svg'
|
||||||
import WindowsKeyIcon from 'enso-assets/windows_key.svg'
|
import WindowsKeyIcon from 'enso-assets/windows_key.svg'
|
||||||
import * as detect from 'enso-common/src/detect'
|
import * as detect from 'enso-common/src/detect'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import type * as dashboardInputBindings from '#/configurations/inputBindings'
|
import type * as dashboardInputBindings from '#/configurations/inputBindings'
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/** @file Permissions for a specific user or user group on a specific asset. */
|
/** @file Permissions for a specific user or user group on a specific asset. */
|
||||||
import * as React from 'react'
|
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 backendHooks from '#/hooks/backendHooks'
|
||||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
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 PeopleIcon from 'enso-assets/people.svg'
|
||||||
import TagIcon from 'enso-assets/tag.svg'
|
import TagIcon from 'enso-assets/tag.svg'
|
||||||
import TimeIcon from 'enso-assets/time.svg'
|
import TimeIcon from 'enso-assets/time.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as backend from '#/services/Backend'
|
import * as backend from '#/services/Backend'
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
* Paywall configuration for different plans.
|
* 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'
|
import * as backend from '#/services/Backend'
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import * as React from 'react'
|
|||||||
|
|
||||||
import * as toastify from 'react-toastify'
|
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 loggerProvider from '#/providers/LoggerProvider'
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
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 ComputerIcon from 'enso-assets/computer.svg'
|
||||||
import RecentIcon from 'enso-assets/recent.svg'
|
import RecentIcon from 'enso-assets/recent.svg'
|
||||||
import Trash2Icon from 'enso-assets/trash2.svg'
|
import Trash2Icon from 'enso-assets/trash2.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as mimeTypes from '#/data/mimeTypes'
|
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 PeopleSettingsIcon from 'enso-assets/people_settings.svg'
|
||||||
import PeopleIcon from 'enso-assets/people.svg'
|
import PeopleIcon from 'enso-assets/people.svg'
|
||||||
import SettingsIcon from 'enso-assets/settings.svg'
|
import SettingsIcon from 'enso-assets/settings.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as inputBindings from '#/configurations/inputBindings'
|
import * as inputBindings from '#/configurations/inputBindings'
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
|||||||
import * as reactQuery from '@tanstack/react-query'
|
import * as reactQuery from '@tanstack/react-query'
|
||||||
import invariant from 'tiny-invariant'
|
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'
|
import * as textProvider from '#/providers/TextProvider'
|
||||||
|
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import LogoIcon from 'enso-assets/enso_logo.svg'
|
import LogoIcon from 'enso-assets/enso_logo.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as backendProvider from '#/providers/BackendProvider'
|
import * as backendProvider from '#/providers/BackendProvider'
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
import * as textProvider from '#/providers/TextProvider'
|
||||||
|
@ -6,8 +6,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import Check from 'enso-assets/check_mark.svg'
|
import Check from 'enso-assets/check_mark.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
import * as textProvider from '#/providers/TextProvider'
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import OpenInNewTabIcon from 'enso-assets/open.svg'
|
import OpenInNewTabIcon from 'enso-assets/open.svg'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
|
|
||||||
import * as appUtils from '#/appUtils'
|
import * as appUtils from '#/appUtils'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import * as textProvider from '#/providers/TextProvider'
|
import * as textProvider from '#/providers/TextProvider'
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file Constants for the subscribe page.
|
* @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'
|
import * as backendModule from '#/services/Backend'
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* React context. */
|
* React context. */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import * as text from '#/text'
|
import * as text from 'enso-common/src/text'
|
||||||
|
|
||||||
import * as object from '#/utilities/object'
|
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
|
* an API endpoint. The functions are asynchronous and return a {@link Promise} that resolves to
|
||||||
* the response from the API. */
|
* the response from the API. */
|
||||||
import * as detect from 'enso-common/src/detect'
|
import * as detect from 'enso-common/src/detect'
|
||||||
|
import type * as text from 'enso-common/src/text'
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import type * as loggerProvider from '#/providers/LoggerProvider'
|
import type * as loggerProvider from '#/providers/LoggerProvider'
|
||||||
import type * as textProvider from '#/providers/TextProvider'
|
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. */
|
/** @file Utilities for manipulating arrays. */
|
||||||
|
|
||||||
// ====================
|
export * from 'enso-common/src/utilities/data/array'
|
||||||
// === 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)
|
|
||||||
}
|
|
||||||
|
@ -1,78 +1,3 @@
|
|||||||
/** @file Utilities for manipulating and displaying dates and times. */
|
/** @file Utilities for manipulating and displaying dates and times. */
|
||||||
import * as newtype from '#/utilities/newtype'
|
|
||||||
|
|
||||||
// =================
|
export * from 'enso-common/src/utilities/data/dateTime'
|
||||||
// === 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())
|
|
||||||
}
|
|
||||||
|
@ -1,64 +1,3 @@
|
|||||||
/** @file Emulates `newtype`s in TypeScript. */
|
/** @file Emulates `newtype`s in TypeScript. */
|
||||||
|
|
||||||
// ===============
|
export * from 'enso-common/src/utilities/data/newtype'
|
||||||
// === 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
|
|
||||||
}
|
|
||||||
|
@ -1,248 +1,3 @@
|
|||||||
/** @file Utilities for working with permissions. */
|
/** @file Utilities for working with permissions. */
|
||||||
import type * as text from '#/text'
|
|
||||||
|
|
||||||
import type * as backend from '#/services/Backend'
|
export * from 'enso-common/src/utilities/permissions'
|
||||||
|
|
||||||
// ========================
|
|
||||||
// === 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 []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,14 +1,3 @@
|
|||||||
/** @file A function that generates a unique string. */
|
/** @file A function that generates a unique string. */
|
||||||
|
|
||||||
// ====================
|
export * from 'enso-common/src/utilities/uniqueString'
|
||||||
// === 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()
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user