mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-21 21:49:51 +01:00
feat(navigation): Search in environments switcher (#25374)
This commit is contained in:
parent
22d2aff23c
commit
eb55c47bd1
@ -1,14 +1,9 @@
|
|||||||
import { IconGear, IconPlus } from '@posthog/icons'
|
import { IconGear, IconPlus } from '@posthog/icons'
|
||||||
import { LemonTag, Spinner } from '@posthog/lemon-ui'
|
import { LemonInput, LemonTag, Spinner } from '@posthog/lemon-ui'
|
||||||
import { useActions, useValues } from 'kea'
|
import { useActions, useValues } from 'kea'
|
||||||
import { router } from 'kea-router'
|
import { router } from 'kea-router'
|
||||||
import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic'
|
import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic'
|
||||||
import {
|
import { LemonMenuItem, LemonMenuOverlay, LemonMenuSection } from 'lib/lemon-ui/LemonMenu/LemonMenu'
|
||||||
LemonMenuItemLeafCallback,
|
|
||||||
LemonMenuItemLeafLink,
|
|
||||||
LemonMenuOverlay,
|
|
||||||
LemonMenuSection,
|
|
||||||
} from 'lib/lemon-ui/LemonMenu/LemonMenu'
|
|
||||||
import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo'
|
import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo'
|
||||||
import { removeFlagIdIfPresent, removeProjectIdIfPresent } from 'lib/utils/router-utils'
|
import { removeFlagIdIfPresent, removeProjectIdIfPresent } from 'lib/utils/router-utils'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
@ -19,17 +14,10 @@ import { urls } from 'scenes/urls'
|
|||||||
import { AvailableFeature } from '~/types'
|
import { AvailableFeature } from '~/types'
|
||||||
|
|
||||||
import { globalModalsLogic } from '../GlobalModals'
|
import { globalModalsLogic } from '../GlobalModals'
|
||||||
|
import { environmentSwitcherLogic } from './environmentsSwitcherLogic'
|
||||||
type MenuItemWithEnvName =
|
|
||||||
| (LemonMenuItemLeafLink & {
|
|
||||||
/** Extra menu item metadata, just for sorting the environments before we display them. */
|
|
||||||
envName: string
|
|
||||||
})
|
|
||||||
| (LemonMenuItemLeafCallback & {
|
|
||||||
envName?: never
|
|
||||||
})
|
|
||||||
|
|
||||||
export function EnvironmentSwitcherOverlay({ onClickInside }: { onClickInside?: () => void }): JSX.Element {
|
export function EnvironmentSwitcherOverlay({ onClickInside }: { onClickInside?: () => void }): JSX.Element {
|
||||||
|
const { sortedProjectsMap } = useValues(environmentSwitcherLogic)
|
||||||
const { currentOrganization, projectCreationForbiddenReason } = useValues(organizationLogic)
|
const { currentOrganization, projectCreationForbiddenReason } = useValues(organizationLogic)
|
||||||
const { currentTeam } = useValues(teamLogic)
|
const { currentTeam } = useValues(teamLogic)
|
||||||
const { guardAvailableFeature } = useValues(upgradeModalLogic)
|
const { guardAvailableFeature } = useValues(upgradeModalLogic)
|
||||||
@ -40,58 +28,14 @@ export function EnvironmentSwitcherOverlay({ onClickInside }: { onClickInside?:
|
|||||||
if (!currentOrganization) {
|
if (!currentOrganization) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const projectMapping = currentOrganization.projects.reduce<Record<number, [string, MenuItemWithEnvName[]]>>(
|
|
||||||
(acc, project) => {
|
|
||||||
acc[project.id] = [project.name, []]
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const team of currentOrganization.teams) {
|
const projectSectionsResult: LemonMenuSection[] = []
|
||||||
const [projectName, envItems] = projectMapping[team.project_id]
|
for (const [projectId, [projectName, projectTeams]] of sortedProjectsMap.entries()) {
|
||||||
envItems.push({
|
const projectItems: LemonMenuItem[] = [
|
||||||
label: (
|
{
|
||||||
<>
|
|
||||||
{team.name}
|
|
||||||
{team.is_demo && (
|
|
||||||
<LemonTag className="ml-1.5" type="highlight">
|
|
||||||
DEMO
|
|
||||||
</LemonTag>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
envName: team.name,
|
|
||||||
active: currentTeam?.id === team.id,
|
|
||||||
to: determineProjectSwitchUrl(location.pathname, team.id),
|
|
||||||
tooltip:
|
|
||||||
currentTeam?.id === team.id
|
|
||||||
? 'Currently active environment'
|
|
||||||
: `Switch to the ${team.name} environment of ${projectName}`,
|
|
||||||
onClick: onClickInside,
|
|
||||||
sideAction: {
|
|
||||||
icon: <IconGear />,
|
|
||||||
tooltip: `Go to ${team.name} settings`,
|
|
||||||
tooltipPlacement: 'right',
|
|
||||||
onClick: onClickInside,
|
|
||||||
to: urls.project(team.id, urls.settings()),
|
|
||||||
},
|
|
||||||
icon: <div className="size-6" />, // Icon-sized filler
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const sortedProjects = Object.entries(projectMapping).sort(
|
|
||||||
// The project with the active environment always comes first - otherwise sorted alphabetically by name
|
|
||||||
([, [aProjectName, aEnvItems]], [, [bProjectName]]) =>
|
|
||||||
aEnvItems.find((item) => item.active) ? -Infinity : aProjectName.localeCompare(bProjectName)
|
|
||||||
)
|
|
||||||
const projectSectionsResult = []
|
|
||||||
for (const [projectId, [projectName, envItems]] of sortedProjects) {
|
|
||||||
// The environment that's active always comes first - otherwise sorted alphabetically by name
|
|
||||||
envItems.sort((a, b) => (b.active ? Infinity : a.envName!.localeCompare(b.envName!)))
|
|
||||||
envItems.unshift({
|
|
||||||
label: projectName,
|
label: projectName,
|
||||||
icon: <UploadedLogo name={projectName} entityId={projectId} outlinedLettermark />,
|
icon: <UploadedLogo name={projectName} entityId={projectId} outlinedLettermark />,
|
||||||
disabledReason: 'Select an environment of this project',
|
disabledReason: 'Select an environment of this project below',
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
sideAction: {
|
sideAction: {
|
||||||
icon: <IconPlus />,
|
icon: <IconPlus />,
|
||||||
@ -106,11 +50,62 @@ export function EnvironmentSwitcherOverlay({ onClickInside }: { onClickInside?:
|
|||||||
},
|
},
|
||||||
'data-attr': 'new-environment-button',
|
'data-attr': 'new-environment-button',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for (const team of projectTeams) {
|
||||||
|
projectItems.push({
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
{team.name}
|
||||||
|
{team.is_demo && (
|
||||||
|
<LemonTag className="ml-1.5" type="highlight">
|
||||||
|
DEMO
|
||||||
|
</LemonTag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
key: team.id,
|
||||||
|
active: currentTeam?.id === team.id,
|
||||||
|
to: determineProjectSwitchUrl(location.pathname, team.id),
|
||||||
|
tooltip:
|
||||||
|
currentTeam?.id === team.id ? (
|
||||||
|
'Currently active environment'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Switch to environment <strong>{team.name}</strong>
|
||||||
|
{currentTeam?.project_id !== team.project_id && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
of project <strong>{projectName}</strong>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onClick: onClickInside,
|
||||||
|
sideAction: {
|
||||||
|
icon: <IconGear />,
|
||||||
|
tooltip: "Go to this environment's settings",
|
||||||
|
tooltipPlacement: 'right',
|
||||||
|
onClick: onClickInside,
|
||||||
|
to: urls.project(team.id, urls.settings()),
|
||||||
|
},
|
||||||
|
icon: <div className="size-6" />, // Icon-sized filler
|
||||||
})
|
})
|
||||||
projectSectionsResult.push({ items: envItems })
|
}
|
||||||
|
projectSectionsResult.push({ key: projectId, items: projectItems })
|
||||||
}
|
}
|
||||||
return projectSectionsResult
|
return projectSectionsResult
|
||||||
}, [currentTeam, currentOrganization, location])
|
}, [
|
||||||
|
currentOrganization,
|
||||||
|
sortedProjectsMap,
|
||||||
|
projectCreationForbiddenReason,
|
||||||
|
onClickInside,
|
||||||
|
guardAvailableFeature,
|
||||||
|
showCreateEnvironmentModal,
|
||||||
|
currentTeam?.id,
|
||||||
|
currentTeam?.project_id,
|
||||||
|
location.pathname,
|
||||||
|
])
|
||||||
|
|
||||||
if (!projectSections) {
|
if (!projectSections) {
|
||||||
return <Spinner />
|
return <Spinner />
|
||||||
@ -119,7 +114,9 @@ export function EnvironmentSwitcherOverlay({ onClickInside }: { onClickInside?:
|
|||||||
return (
|
return (
|
||||||
<LemonMenuOverlay
|
<LemonMenuOverlay
|
||||||
items={[
|
items={[
|
||||||
{ title: 'Projects', items: [] },
|
{
|
||||||
|
items: [{ label: EnvironmentSwitcherSearch }],
|
||||||
|
},
|
||||||
...projectSections,
|
...projectSections,
|
||||||
{
|
{
|
||||||
icon: <IconPlus />,
|
icon: <IconPlus />,
|
||||||
@ -127,7 +124,6 @@ export function EnvironmentSwitcherOverlay({ onClickInside }: { onClickInside?:
|
|||||||
disabledReason: projectCreationForbiddenReason,
|
disabledReason: projectCreationForbiddenReason,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onClickInside?.()
|
onClickInside?.()
|
||||||
// TODO: Use showCreateEnvironmentModal
|
|
||||||
guardAvailableFeature(AvailableFeature.ORGANIZATIONS_PROJECTS, showCreateProjectModal, {
|
guardAvailableFeature(AvailableFeature.ORGANIZATIONS_PROJECTS, showCreateProjectModal, {
|
||||||
currentUsage: currentOrganization?.teams?.length,
|
currentUsage: currentOrganization?.teams?.length,
|
||||||
})
|
})
|
||||||
@ -148,3 +144,22 @@ function determineProjectSwitchUrl(pathname: string, newTeamId: number): string
|
|||||||
route = removeFlagIdIfPresent(route)
|
route = removeFlagIdIfPresent(route)
|
||||||
return urls.project(newTeamId, route)
|
return urls.project(newTeamId, route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EnvironmentSwitcherSearch(): JSX.Element {
|
||||||
|
const { environmentSwitcherSearch } = useValues(environmentSwitcherLogic)
|
||||||
|
const { setEnvironmentSwitcherSearch } = useActions(environmentSwitcherLogic)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LemonInput
|
||||||
|
value={environmentSwitcherSearch}
|
||||||
|
onChange={setEnvironmentSwitcherSearch}
|
||||||
|
type="search"
|
||||||
|
autoFocus
|
||||||
|
placeholder="Search projects & environments"
|
||||||
|
className="min-w-64"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation() // Prevent dropdown from closing
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
145
frontend/src/layout/navigation/environmentsSwitcherLogic.tsx
Normal file
145
frontend/src/layout/navigation/environmentsSwitcherLogic.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import FuseClass from 'fuse.js'
|
||||||
|
import { actions, connect, kea, path, reducers, selectors } from 'kea'
|
||||||
|
import { organizationLogic } from 'scenes/organizationLogic'
|
||||||
|
import { teamLogic } from 'scenes/teamLogic'
|
||||||
|
import { userLogic } from 'scenes/userLogic'
|
||||||
|
|
||||||
|
import { ProjectBasicType, TeamBasicType } from '~/types'
|
||||||
|
|
||||||
|
import type { environmentSwitcherLogicType } from './environmentsSwitcherLogicType'
|
||||||
|
|
||||||
|
// Helping kea-typegen navigate the exported default class for Fuse
|
||||||
|
export interface Fuse<T> extends FuseClass<T> {}
|
||||||
|
|
||||||
|
export type ProjectsMap = Map<
|
||||||
|
TeamBasicTypeWithProjectName['project_id'],
|
||||||
|
[TeamBasicTypeWithProjectName['project_name'], TeamBasicTypeWithProjectName[]]
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface TeamBasicTypeWithProjectName extends TeamBasicType {
|
||||||
|
project_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const environmentSwitcherLogic = kea<environmentSwitcherLogicType>([
|
||||||
|
path(['layout', 'navigation', 'environmentsSwitcherLogic']),
|
||||||
|
connect({
|
||||||
|
values: [userLogic, ['user'], teamLogic, ['currentTeam'], organizationLogic, ['currentOrganization']],
|
||||||
|
}),
|
||||||
|
actions({
|
||||||
|
setEnvironmentSwitcherSearch: (input: string) => ({ input }),
|
||||||
|
}),
|
||||||
|
reducers({
|
||||||
|
environmentSwitcherSearch: [
|
||||||
|
'',
|
||||||
|
{
|
||||||
|
setEnvironmentSwitcherSearch: (_, { input }) => input,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
selectors({
|
||||||
|
allTeamsSorted: [
|
||||||
|
(s) => [s.currentOrganization, s.currentTeam],
|
||||||
|
(currentOrganization, currentTeam): TeamBasicTypeWithProjectName[] => {
|
||||||
|
const collection: TeamBasicTypeWithProjectName[] = []
|
||||||
|
if (currentOrganization) {
|
||||||
|
const projectIdToName = Object.fromEntries(
|
||||||
|
currentOrganization.projects.map((project) => [project.id, project.name])
|
||||||
|
)
|
||||||
|
for (const team of currentOrganization.teams) {
|
||||||
|
collection.push({
|
||||||
|
...team,
|
||||||
|
project_name: projectIdToName[team.project_id],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collection.sort((a, b) => {
|
||||||
|
// Sorting logic:
|
||||||
|
// 1. first by whether the team is the current team,
|
||||||
|
// 2. then by whether the project is the current project,
|
||||||
|
// 3. then by project name,
|
||||||
|
// 4. then by team name
|
||||||
|
if (a.id === currentTeam?.id) {
|
||||||
|
return -1
|
||||||
|
} else if (b.id === currentTeam?.id) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (a.project_id !== b.project_id) {
|
||||||
|
if (a.project_id === currentTeam?.project_id) {
|
||||||
|
return -1
|
||||||
|
} else if (b.project_id === currentTeam?.project_id) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return a.project_name.localeCompare(b.project_name)
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
return collection
|
||||||
|
},
|
||||||
|
],
|
||||||
|
teamsFuse: [
|
||||||
|
(s) => [s.allTeamsSorted],
|
||||||
|
(allTeamsSorted): Fuse<TeamBasicTypeWithProjectName> => {
|
||||||
|
return new FuseClass(allTeamsSorted, { keys: ['name', 'project_name'] })
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectsSorted: [
|
||||||
|
(s) => [s.currentOrganization, s.currentTeam],
|
||||||
|
(currentOrganization, currentTeam): ProjectBasicType[] => {
|
||||||
|
// Includes projects that have no environments
|
||||||
|
if (!currentOrganization) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const collection: ProjectBasicType[] = currentOrganization.projects.slice()
|
||||||
|
collection.sort((a, b) => {
|
||||||
|
// Sorting logic: 1. first by whether the project is the current project, 2. then by project name
|
||||||
|
if (a.id === currentTeam?.id) {
|
||||||
|
return -1
|
||||||
|
} else if (b.id === currentTeam?.id) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
return collection
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sortedProjectsMap: [
|
||||||
|
(s) => [s.projectsSorted, s.allTeamsSorted, s.teamsFuse, s.environmentSwitcherSearch],
|
||||||
|
(projectsSorted, allTeamsSorted, teamsFuse, environmentSwitcherSearch): ProjectsMap => {
|
||||||
|
// Using a map so that insertion order is preserved
|
||||||
|
// (JS objects don't preserve the order for keys that are numbers)
|
||||||
|
const projectsWithTeamsSorted: ProjectsMap = new Map()
|
||||||
|
|
||||||
|
if (environmentSwitcherSearch) {
|
||||||
|
const matchingTeams = teamsFuse.search(environmentSwitcherSearch).map((result) => result.item)
|
||||||
|
const projectIdToTopTeamRank = matchingTeams.reduce<Record<TeamBasicType['project_id'], number>>(
|
||||||
|
(acc, team, index) => {
|
||||||
|
if (!acc[team.project_id]) {
|
||||||
|
acc[team.project_id] = index
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
matchingTeams.sort(
|
||||||
|
(a, b) => projectIdToTopTeamRank[a.project_id] - projectIdToTopTeamRank[b.project_id]
|
||||||
|
)
|
||||||
|
for (const team of matchingTeams) {
|
||||||
|
if (!projectsWithTeamsSorted.has(team.project_id)) {
|
||||||
|
projectsWithTeamsSorted.set(team.project_id, [team.project_name, []])
|
||||||
|
}
|
||||||
|
projectsWithTeamsSorted.get(team.project_id)![1].push(team)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const project of projectsSorted) {
|
||||||
|
projectsWithTeamsSorted.set(project.id, [project.name, []])
|
||||||
|
}
|
||||||
|
for (const team of allTeamsSorted) {
|
||||||
|
projectsWithTeamsSorted.get(team.project_id)![1].push(team)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectsWithTeamsSorted
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
])
|
@ -14,6 +14,7 @@ interface LemonInputPropsBase
|
|||||||
// NOTE: We explicitly pick rather than omit to ensure these components aren't used incorrectly
|
// NOTE: We explicitly pick rather than omit to ensure these components aren't used incorrectly
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
| 'className'
|
| 'className'
|
||||||
|
| 'onClick'
|
||||||
| 'onFocus'
|
| 'onFocus'
|
||||||
| 'onBlur'
|
| 'onBlur'
|
||||||
| 'autoFocus'
|
| 'autoFocus'
|
||||||
|
@ -16,6 +16,7 @@ export interface LemonMenuItemBase
|
|||||||
'icon' | 'sideIcon' | 'sideAction' | 'disabledReason' | 'tooltip' | 'active' | 'status' | 'data-attr'
|
'icon' | 'sideIcon' | 'sideAction' | 'disabledReason' | 'tooltip' | 'active' | 'status' | 'data-attr'
|
||||||
> {
|
> {
|
||||||
label: string | JSX.Element
|
label: string | JSX.Element
|
||||||
|
key?: React.Key
|
||||||
/** True if the item is a custom element. */
|
/** True if the item is a custom element. */
|
||||||
custom?: boolean
|
custom?: boolean
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ export type LemonMenuItemLeaf = LemonMenuItemLeafCallback | LemonMenuItemLeafLin
|
|||||||
export interface LemonMenuItemCustom {
|
export interface LemonMenuItemCustom {
|
||||||
/** A label that's a component means it will be rendered directly, and not wrapped in a button. */
|
/** A label that's a component means it will be rendered directly, and not wrapped in a button. */
|
||||||
label: () => JSX.Element
|
label: () => JSX.Element
|
||||||
|
key?: React.Key
|
||||||
active?: never
|
active?: never
|
||||||
items?: never
|
items?: never
|
||||||
keyboardShortcut?: never
|
keyboardShortcut?: never
|
||||||
@ -57,6 +59,7 @@ export type LemonMenuItem = LemonMenuItemLeaf | LemonMenuItemCustom | LemonMenuI
|
|||||||
|
|
||||||
export interface LemonMenuSection {
|
export interface LemonMenuSection {
|
||||||
title?: string | React.ReactNode
|
title?: string | React.ReactNode
|
||||||
|
key?: React.Key
|
||||||
items: (LemonMenuItem | false | null)[]
|
items: (LemonMenuItem | false | null)[]
|
||||||
footer?: string | React.ReactNode
|
footer?: string | React.ReactNode
|
||||||
}
|
}
|
||||||
@ -171,7 +174,7 @@ export function LemonMenuSectionList({
|
|||||||
<ul>
|
<ul>
|
||||||
{sections.map((section, i) => {
|
{sections.map((section, i) => {
|
||||||
const sectionElement = (
|
const sectionElement = (
|
||||||
<li key={i}>
|
<li key={section.key || i}>
|
||||||
<section className="space-y-px">
|
<section className="space-y-px">
|
||||||
{section.title ? (
|
{section.title ? (
|
||||||
typeof section.title === 'string' ? (
|
typeof section.title === 'string' ? (
|
||||||
@ -221,7 +224,7 @@ export function LemonMenuItemList({
|
|||||||
return (
|
return (
|
||||||
<ul className="space-y-px">
|
<ul className="space-y-px">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<li key={index}>
|
<li key={item.key || index}>
|
||||||
<LemonMenuItemButton
|
<LemonMenuItemButton
|
||||||
item={item}
|
item={item}
|
||||||
size={buttonSize}
|
size={buttonSize}
|
||||||
|
@ -36,7 +36,7 @@ export const Lettermark = React.forwardRef<HTMLDivElement, LettermarkProps>(func
|
|||||||
const representation = name
|
const representation = name
|
||||||
? typeof name === 'number'
|
? typeof name === 'number'
|
||||||
? String(Math.floor(name))
|
? String(Math.floor(name))
|
||||||
: name.toLocaleUpperCase().charAt(0)
|
: String.fromCodePoint(name.codePointAt(0)!).toLocaleUpperCase()
|
||||||
: '?'
|
: '?'
|
||||||
|
|
||||||
const colorIndex = color ? color : typeof index === 'number' ? (index % NUM_LETTERMARK_STYLES) + 1 : undefined
|
const colorIndex = color ? color : typeof index === 'number' ? (index % NUM_LETTERMARK_STYLES) + 1 : undefined
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"resolveJsonModule": true, // Include modules imported with .json extension
|
"resolveJsonModule": true, // Include modules imported with .json extension
|
||||||
"noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking)
|
"noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking)
|
||||||
"jsx": "react-jsx", // Support JSX in .tsx files
|
"jsx": "react-jsx", // Support JSX in .tsx files
|
||||||
|
"target": "ES2015",
|
||||||
"sourceMap": true, // Generate corrresponding .map file
|
"sourceMap": true, // Generate corrresponding .map file
|
||||||
"declaration": true, // Generate corresponding .d.ts file
|
"declaration": true, // Generate corresponding .d.ts file
|
||||||
"noUnusedLocals": true, // Report errors on unused locals
|
"noUnusedLocals": true, // Report errors on unused locals
|
||||||
|
Loading…
Reference in New Issue
Block a user