mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 13:21:35 +03:00
Improve label interactions (#8417)
- Fixes https://github.com/enso-org/cloud-v2/issues/781 - Implement parser for search query - Change all UI (labels column, labels side panel, searcher in top bar) to use search query parser - Change "remove label from asset" to right click - Also add a tooltip to notify user that right click removes the label - Change click action on label on asset, to toggle the label from the search - Stop sending the list of labels for filtering on the server side Unrelated changes: - Switch dashboard dev server to use Vite. All other servers should be unaffected. - `ide watch` works - `ide build` works - `gui watch` works - `dashboard build` works # Important Notes There are quite a lot of new interactions with the search bar which should probably all be tested.
This commit is contained in:
parent
dff1c0c88b
commit
1e93e69523
@ -56,12 +56,15 @@ export function esbuildPluginGenerateTailwind(): esbuild.Plugin {
|
||||
return {
|
||||
name: 'enso-generate-tailwind',
|
||||
setup: build => {
|
||||
const cssProcessor = postcss([
|
||||
const cssProcessor = postcss(
|
||||
tailwindcss({
|
||||
config: tailwindConfig,
|
||||
...tailwindConfig.default,
|
||||
content: tailwindConfig.default.content.map(glob =>
|
||||
glob.replace(/^[.][/]/, THIS_PATH + '/')
|
||||
),
|
||||
}),
|
||||
tailwindcssNesting(),
|
||||
])
|
||||
tailwindcssNesting()
|
||||
)
|
||||
build.onLoad({ filter: /tailwind\.css$/ }, async loadArgs => {
|
||||
// console.log(`Processing CSS file '${loadArgs.path}'.`)
|
||||
const content = await fs.readFile(loadArgs.path, 'utf8')
|
||||
|
58
app/ide-desktop/lib/dashboard/index.html
Normal file
58
app/ide-desktop/lib/dashboard/index.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!--
|
||||
FIXME [NP]: https://github.com/enso-org/cloud-v2/issues/345
|
||||
This file is used by both the `content` and `dashboard` packages. The `dashboard` package uses it
|
||||
via a symlink. This is temporary, while the `content` and `dashboard` have separate entrypoints
|
||||
for cloud and desktop. Once they are merged, the symlink must be removed.
|
||||
-->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<!-- FIXME https://github.com/validator/validator/issues/917 -->
|
||||
<!-- FIXME Security Vulnerabilities: https://github.com/enso-org/ide/issues/226 -->
|
||||
<!-- NOTE `frame-src` section of `http-equiv` required only for authorization -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
frame-src 'self' data: https://accounts.google.com https://enso-org.firebaseapp.com;
|
||||
script-src 'self' 'unsafe-eval' data: https://*;
|
||||
style-src 'self' 'unsafe-inline' data: https://*;
|
||||
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
|
||||
worker-src 'self' blob:;
|
||||
img-src 'self' blob: data: https://*;
|
||||
font-src 'self' data: https://*"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="
|
||||
width=device-width,
|
||||
initial-scale = 1.0,
|
||||
maximum-scale = 1.0,
|
||||
user-scalable = no"
|
||||
/>
|
||||
<title>Enso</title>
|
||||
<!-- Generated by the build script based on the Enso Font package. -->
|
||||
<link rel="stylesheet" href="./src/ensoFont.css" />
|
||||
<script type="module" src="./src/index.ts" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="enso-dashboard" class="enso-dashboard"></div>
|
||||
<div id="enso-chat" class="enso-chat"></div>
|
||||
<noscript>
|
||||
This page requires JavaScript to run. Please enable it in your browser.
|
||||
</noscript>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.2/dist/twemoji.min.js"
|
||||
integrity="sha384-D6GSzpW7fMH86ilu73eB95ipkfeXcMPoOGVst/L04yqSSe+RTUY0jXcuEIZk0wrT"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-CLTBJ37MDM"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"typecheck": "tsc",
|
||||
"build": "tsx bundle.ts",
|
||||
"dev": "tsx watch.ts",
|
||||
"dev": "vite",
|
||||
"start": "tsx start.ts",
|
||||
"test": "npm run test:unit",
|
||||
"test:unit": "playwright test",
|
||||
@ -43,10 +43,12 @@
|
||||
"eslint-plugin-jsdoc": "^46.8.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"playwright": "^1.38.0",
|
||||
"postcss": "^8.4.29",
|
||||
"react-toastify": "^9.1.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"tsx": "^3.12.6",
|
||||
"typescript": "~5.2.2"
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-x64": "^0.17.15",
|
||||
|
9
app/ide-desktop/lib/dashboard/postcss.config.js
Normal file
9
app/ide-desktop/lib/dashboard/postcss.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
/** @file Configuration for PostCSS. */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
export default {
|
||||
plugins: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
/** @file Parsing and representation of the search query. */
|
||||
|
||||
// ==================
|
||||
// === AssetQuery ===
|
||||
// ==================
|
||||
|
||||
/** An {@link AssetQuery}, without the query and methods. */
|
||||
interface AssetQueryData extends Omit<AssetQuery, 'add' | 'query' | 'remove'> {}
|
||||
|
||||
/** An individual segment of a query string input to {@link AssetQuery}. */
|
||||
interface AssetQueryTerm {
|
||||
tag: string | null
|
||||
value: string
|
||||
}
|
||||
|
||||
/** Parsing and representation of the search query. */
|
||||
export class AssetQuery {
|
||||
static termsRegex =
|
||||
// Control characters must be handled, in order to follow the JSON spec.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/(?:([^\s:]*):)?(?:("(?:[^\0-\x1f\\"]|\\[\\/bfnrt"]|\\u[0-9a-fA-F]{4})*")|([^\s"]\S*|))/g
|
||||
static plainValueRegex = /^(?:|[^"]\S*)$/u
|
||||
|
||||
/** Create an {@link AssetQuery}. */
|
||||
constructor(
|
||||
readonly query: string,
|
||||
readonly keywords: string[],
|
||||
readonly labels: string[]
|
||||
) {}
|
||||
|
||||
/** Return a list of {@link AssetQueryTerm}s found in the raw user input string. */
|
||||
static terms(query: string): AssetQueryTerm[] {
|
||||
const terms: AssetQueryTerm[] = []
|
||||
for (const [, tag, jsonValue, plainValue] of query.trim().matchAll(this.termsRegex)) {
|
||||
if (tag != null || plainValue == null || plainValue !== '') {
|
||||
terms.push({
|
||||
tag: tag ?? null,
|
||||
value: jsonValue != null ? String(JSON.parse(jsonValue)) : plainValue ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
return terms
|
||||
}
|
||||
|
||||
/** Convert an {@link AssetQueryTerm} to a string usable in a raw user input string. */
|
||||
static termToString(term: AssetQueryTerm) {
|
||||
const tagSegment = term.tag == null ? '' : term.tag + ':'
|
||||
const valueSegment = this.plainValueRegex.test(term.value)
|
||||
? term.value
|
||||
: JSON.stringify(term.value)
|
||||
return tagSegment + valueSegment
|
||||
}
|
||||
|
||||
/** Create an {@link AssetQuery} from a raw user input string. */
|
||||
static fromString(query: string): AssetQuery {
|
||||
const terms = AssetQuery.terms(query)
|
||||
const keywords = terms
|
||||
.filter(term => term.tag == null || term.tag === '')
|
||||
.map(term => term.value)
|
||||
const labels = terms
|
||||
.filter(term => term.tag?.toLowerCase() === 'label')
|
||||
.map(term => term.value)
|
||||
return new AssetQuery(query, keywords, labels)
|
||||
}
|
||||
|
||||
/** Return a new {@link AssetQuery} with the specified terms added,
|
||||
* or itself if there are no terms to remove. */
|
||||
add(values: Partial<AssetQueryData>): AssetQuery {
|
||||
const { keywords, labels } = values
|
||||
const noKeywords = !keywords || keywords.length === 0
|
||||
const noLabels = !labels || labels.length === 0
|
||||
if (noKeywords && noLabels) {
|
||||
return this
|
||||
} else {
|
||||
const newKeywords = this.keywords
|
||||
let addedKeywords: string[] = []
|
||||
if (!noKeywords) {
|
||||
const keywordsSet = new Set(this.keywords)
|
||||
addedKeywords = keywords.filter(keyword => !keywordsSet.has(keyword))
|
||||
newKeywords.push(...addedKeywords)
|
||||
}
|
||||
const newLabels = this.labels
|
||||
let addedLabels: string[] = []
|
||||
if (!noLabels) {
|
||||
const labelsSet = new Set(this.labels)
|
||||
addedLabels = labels.filter(keyword => !labelsSet.has(keyword))
|
||||
newLabels.push(...addedLabels)
|
||||
}
|
||||
const newQuery =
|
||||
this.query +
|
||||
(this.query === '' ? '' : ' ') +
|
||||
[
|
||||
...addedKeywords.map(keyword =>
|
||||
AssetQuery.termToString({ tag: null, value: keyword })
|
||||
),
|
||||
...addedLabels.map(label =>
|
||||
AssetQuery.termToString({ tag: 'label', value: label })
|
||||
),
|
||||
].join(' ')
|
||||
return new AssetQuery(newQuery, newKeywords, newLabels)
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a new {@link AssetQuery} with the specified terms removed,
|
||||
* or itself if there are no terms to remove. */
|
||||
delete(values: Partial<AssetQueryData>): AssetQuery {
|
||||
const { keywords, labels } = values
|
||||
const noKeywords = !keywords || keywords.length === 0
|
||||
const noLabels = !labels || labels.length === 0
|
||||
if (noKeywords && noLabels) {
|
||||
return this
|
||||
} else {
|
||||
let newKeywords = this.keywords
|
||||
const keywordsSet = new Set(keywords ?? [])
|
||||
if (!noKeywords) {
|
||||
newKeywords = newKeywords.filter(keyword => !keywordsSet.has(keyword))
|
||||
}
|
||||
let newLabels = this.labels
|
||||
const labelsSet = new Set(labels ?? [])
|
||||
if (!noLabels) {
|
||||
newLabels = newLabels.filter(label => !labelsSet.has(label))
|
||||
}
|
||||
if (
|
||||
newKeywords.length === this.keywords.length &&
|
||||
newLabels.length === this.labels.length
|
||||
) {
|
||||
return this
|
||||
} else {
|
||||
const newQuery = AssetQuery.terms(this.query)
|
||||
.filter(term => {
|
||||
switch (term.tag?.toLowerCase() ?? null) {
|
||||
case null:
|
||||
case '': {
|
||||
return !keywordsSet.has(term.value)
|
||||
}
|
||||
case 'label': {
|
||||
return !labelsSet.has(term.value)
|
||||
}
|
||||
default: {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(term => AssetQuery.termToString(term))
|
||||
.join(' ')
|
||||
return new AssetQuery(newQuery, newKeywords, newLabels)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -243,7 +243,7 @@ function LabelsColumn(props: AssetColumnProps) {
|
||||
const {
|
||||
item: { item: asset },
|
||||
setItem,
|
||||
state: { category, labels, deletedLabelNames, doCreateLabel },
|
||||
state: { category, labels, setQuery, deletedLabelNames, doCreateLabel },
|
||||
rowState: { temporarilyAddedLabels, temporarilyRemovedLabels },
|
||||
} = props
|
||||
const session = authProvider.useNonPartialUserSession()
|
||||
@ -294,7 +294,9 @@ function LabelsColumn(props: AssetColumnProps) {
|
||||
? 'relative before:absolute before:rounded-full before:border-2 before:border-delete before:inset-0 before:w-full before:h-full'
|
||||
: ''
|
||||
}
|
||||
onClick={() => {
|
||||
onContextMenu={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setAsset(oldAsset => {
|
||||
const newLabels =
|
||||
oldAsset.labels?.filter(oldLabel => oldLabel !== label) ?? []
|
||||
@ -319,6 +321,15 @@ function LabelsColumn(props: AssetColumnProps) {
|
||||
}
|
||||
})
|
||||
}}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setQuery(oldQuery =>
|
||||
oldQuery.labels.includes(label)
|
||||
? oldQuery.delete({ labels: [label] })
|
||||
: oldQuery.add({ labels: [label] })
|
||||
)
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
|
@ -5,6 +5,7 @@ import * as toast from 'react-toastify'
|
||||
import * as array from '../array'
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import type * as assetQuery from '../../assetQuery'
|
||||
import * as assetTreeNode from '../assetTreeNode'
|
||||
import * as backendModule from '../backend'
|
||||
import * as columnModule from '../column'
|
||||
@ -149,6 +150,8 @@ export interface AssetsTableState {
|
||||
setSortColumn: (column: columnModule.SortableColumn | null) => void
|
||||
sortDirection: sorting.SortDirection | null
|
||||
setSortDirection: (sortDirection: sorting.SortDirection | null) => void
|
||||
query: assetQuery.AssetQuery
|
||||
setQuery: React.Dispatch<React.SetStateAction<assetQuery.AssetQuery>>
|
||||
dispatchAssetListEvent: (event: assetListEventModule.AssetListEvent) => void
|
||||
assetEvents: assetEventModule.AssetEvent[]
|
||||
dispatchAssetEvent: (event: assetEventModule.AssetEvent) => void
|
||||
@ -197,10 +200,10 @@ export const INITIAL_ROW_STATE = Object.freeze<AssetRowState>({
|
||||
|
||||
/** Props for a {@link AssetsTable}. */
|
||||
export interface AssetsTableProps {
|
||||
query: string
|
||||
query: assetQuery.AssetQuery
|
||||
setQuery: React.Dispatch<React.SetStateAction<assetQuery.AssetQuery>>
|
||||
category: categorySwitcher.Category
|
||||
allLabels: Map<backendModule.LabelName, backendModule.Label>
|
||||
currentLabels: backendModule.LabelName[] | null
|
||||
initialProjectName: string | null
|
||||
projectStartupInfo: backendModule.ProjectStartupInfo | null
|
||||
deletedLabelNames: Set<backendModule.LabelName>
|
||||
@ -228,9 +231,9 @@ export interface AssetsTableProps {
|
||||
export default function AssetsTable(props: AssetsTableProps) {
|
||||
const {
|
||||
query,
|
||||
setQuery,
|
||||
category,
|
||||
allLabels,
|
||||
currentLabels,
|
||||
deletedLabelNames,
|
||||
initialProjectName,
|
||||
projectStartupInfo,
|
||||
@ -277,11 +280,17 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
[backend, organization]
|
||||
)
|
||||
const filter = React.useMemo(() => {
|
||||
if (query === '') {
|
||||
if (query.query === '') {
|
||||
return null
|
||||
} else {
|
||||
const regex = new RegExp(string.regexEscape(query), 'i')
|
||||
return (node: assetTreeNode.AssetTreeNode) => regex.test(node.item.title)
|
||||
return (node: assetTreeNode.AssetTreeNode) => {
|
||||
const labels: string[] = node.item.labels ?? []
|
||||
const lowercaseName = node.item.title.toLowerCase()
|
||||
return (
|
||||
query.labels.every(label => labels.includes(label)) &&
|
||||
query.keywords.every(keyword => lowercaseName.includes(keyword.toLowerCase()))
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [query])
|
||||
const displayItems = React.useMemo(() => {
|
||||
@ -465,7 +474,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
parentId: null,
|
||||
filterBy: CATEGORY_TO_FILTER_BY[category],
|
||||
recentProjects: category === categorySwitcher.Category.recent,
|
||||
labels: currentLabels,
|
||||
labels: null,
|
||||
},
|
||||
null
|
||||
)
|
||||
@ -529,7 +538,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
filterBy: CATEGORY_TO_FILTER_BY[category],
|
||||
recentProjects:
|
||||
category === categorySwitcher.Category.recent,
|
||||
labels: currentLabels,
|
||||
labels: null,
|
||||
},
|
||||
entry.item.title
|
||||
)
|
||||
@ -572,7 +581,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
parentId: null,
|
||||
filterBy: CATEGORY_TO_FILTER_BY[category],
|
||||
recentProjects: category === categorySwitcher.Category.recent,
|
||||
labels: currentLabels,
|
||||
labels: null,
|
||||
},
|
||||
null
|
||||
)
|
||||
@ -587,7 +596,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[category, currentLabels, accessToken, organization, backend]
|
||||
[category, accessToken, organization, backend]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
@ -685,7 +694,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
parentId: directoryId,
|
||||
filterBy: CATEGORY_TO_FILTER_BY[category],
|
||||
recentProjects: category === categorySwitcher.Category.recent,
|
||||
labels: currentLabels,
|
||||
labels: null,
|
||||
},
|
||||
title ?? null
|
||||
)
|
||||
@ -752,7 +761,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
})()
|
||||
}
|
||||
},
|
||||
[category, currentLabels, backend]
|
||||
[category, backend]
|
||||
)
|
||||
|
||||
const getNewProjectName = React.useCallback(
|
||||
@ -1274,6 +1283,8 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
setSortColumn,
|
||||
sortDirection,
|
||||
setSortDirection,
|
||||
query,
|
||||
setQuery,
|
||||
assetEvents,
|
||||
dispatchAssetEvent,
|
||||
dispatchAssetListEvent,
|
||||
@ -1296,6 +1307,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
assetEvents,
|
||||
query,
|
||||
doToggleDirectoryExpansion,
|
||||
doOpenManually,
|
||||
doOpenIde,
|
||||
@ -1303,6 +1315,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
doCreateLabel,
|
||||
doCut,
|
||||
doPaste,
|
||||
/* should never change */ setQuery,
|
||||
/* should never change */ setSortColumn,
|
||||
/* should never change */ setSortDirection,
|
||||
/* should never change */ dispatchAssetEvent,
|
||||
@ -1389,7 +1402,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
placeholder={
|
||||
category === categorySwitcher.Category.trash
|
||||
? TRASH_PLACEHOLDER
|
||||
: query !== '' || currentLabels != null
|
||||
: query.query !== ''
|
||||
? QUERY_PLACEHOLDER
|
||||
: PLACEHOLDER
|
||||
}
|
||||
@ -1428,7 +1441,7 @@ export default function AssetsTable(props: AssetsTableProps) {
|
||||
setModal(
|
||||
<DragModal
|
||||
event={event}
|
||||
className="flex flex-col bg-frame rounded-2xl bg-frame-selected backdrop-blur-3xl"
|
||||
className="flex flex-col rounded-2xl bg-frame-selected backdrop-blur-3xl"
|
||||
doCleanup={() => {
|
||||
drag.ASSET_ROWS.unbind(payload)
|
||||
}}
|
||||
|
@ -4,6 +4,7 @@ import * as React from 'react'
|
||||
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import * as assetQuery from '../../assetQuery'
|
||||
import * as backendModule from '../backend'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as http from '../../http'
|
||||
@ -59,7 +60,7 @@ export default function Dashboard(props: DashboardProps) {
|
||||
const { localStorage } = localStorageProvider.useLocalStorage()
|
||||
const { shortcuts } = shortcutsProvider.useShortcuts()
|
||||
const [initialized, setInitialized] = React.useState(false)
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [query, setQuery] = React.useState(() => assetQuery.AssetQuery.fromString(''))
|
||||
const [isHelpChatOpen, setIsHelpChatOpen] = React.useState(false)
|
||||
const [isHelpChatVisible, setIsHelpChatVisible] = React.useState(false)
|
||||
const [loadingProjectManagerDidFail, setLoadingProjectManagerDidFail] = React.useState(false)
|
||||
@ -99,7 +100,7 @@ export default function Dashboard(props: DashboardProps) {
|
||||
}, [page, /* should never change */ unsetModal])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query !== '') {
|
||||
if (query.query !== '') {
|
||||
setPage(pageSwitcher.Page.drive)
|
||||
}
|
||||
}, [query])
|
||||
@ -412,6 +413,7 @@ export default function Dashboard(props: DashboardProps) {
|
||||
page={page}
|
||||
initialProjectName={initialProjectName}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
projectStartupInfo={projectStartupInfo}
|
||||
queuedAssetEvents={queuedAssetEvents}
|
||||
assetListEvents={assetListEvents}
|
||||
|
@ -6,11 +6,11 @@ import * as common from 'enso-common'
|
||||
import * as appInfo from '../../appInfo'
|
||||
import * as assetEventModule from '../events/assetEvent'
|
||||
import * as assetListEventModule from '../events/assetListEvent'
|
||||
import type * as assetQuery from '../../assetQuery'
|
||||
import * as authProvider from '../../authentication/providers/auth'
|
||||
import * as backendModule from '../backend'
|
||||
import * as backendProvider from '../../providers/backend'
|
||||
import * as hooks from '../../hooks'
|
||||
import * as identity from '../identity'
|
||||
import * as localStorageModule from '../localStorage'
|
||||
import * as localStorageProvider from '../../providers/localStorage'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
@ -41,7 +41,8 @@ export interface DriveProps {
|
||||
dispatchAssetListEvent: (directoryEvent: assetListEventModule.AssetListEvent) => void
|
||||
assetEvents: assetEventModule.AssetEvent[]
|
||||
dispatchAssetEvent: (directoryEvent: assetEventModule.AssetEvent) => void
|
||||
query: string
|
||||
query: assetQuery.AssetQuery
|
||||
setQuery: React.Dispatch<React.SetStateAction<assetQuery.AssetQuery>>
|
||||
projectStartupInfo: backendModule.ProjectStartupInfo | null
|
||||
doCreateProject: (templateId: string | null) => void
|
||||
doOpenEditor: (
|
||||
@ -65,6 +66,7 @@ export default function Drive(props: DriveProps) {
|
||||
initialProjectName,
|
||||
queuedAssetEvents,
|
||||
query,
|
||||
setQuery,
|
||||
projectStartupInfo,
|
||||
assetListEvents,
|
||||
dispatchAssetListEvent,
|
||||
@ -91,7 +93,7 @@ export default function Drive(props: DriveProps) {
|
||||
categorySwitcher.Category.home
|
||||
)
|
||||
const [labels, setLabels] = React.useState<backendModule.Label[]>([])
|
||||
const [currentLabels, setCurrentLabels] = React.useState<backendModule.LabelName[] | null>(null)
|
||||
// const [currentLabels, setCurrentLabels] = React.useState<backendModule.LabelName[] | null>(null)
|
||||
const [newLabelNames, setNewLabelNames] = React.useState(new Set<backendModule.LabelName>())
|
||||
const [deletedLabelNames, setDeletedLabelNames] = React.useState(
|
||||
new Set<backendModule.LabelName>()
|
||||
@ -186,36 +188,11 @@ export default function Drive(props: DriveProps) {
|
||||
oldLabel.id === placeholderLabel.id ? newLabel : oldLabel
|
||||
)
|
||||
)
|
||||
setCurrentLabels(oldLabels => {
|
||||
let found = identity.identity<boolean>(false)
|
||||
const newLabels =
|
||||
oldLabels?.map(oldLabel => {
|
||||
if (oldLabel === placeholderLabel.value) {
|
||||
found = true
|
||||
return newLabel.value
|
||||
} else {
|
||||
return oldLabel
|
||||
}
|
||||
}) ?? null
|
||||
return found ? newLabels : oldLabels
|
||||
})
|
||||
} catch (error) {
|
||||
toastAndLog(null, error)
|
||||
setLabels(oldLabels =>
|
||||
oldLabels.filter(oldLabel => oldLabel.id !== placeholderLabel.id)
|
||||
)
|
||||
setCurrentLabels(oldLabels => {
|
||||
let found = identity.identity<boolean>(false)
|
||||
const newLabels = (oldLabels ?? []).filter(oldLabel => {
|
||||
if (oldLabel === placeholderLabel.value) {
|
||||
found = true
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
return found ? (newLabels.length === 0 ? null : newLabels) : oldLabels
|
||||
})
|
||||
}
|
||||
setNewLabelNames(
|
||||
labelNames =>
|
||||
@ -228,22 +205,7 @@ export default function Drive(props: DriveProps) {
|
||||
const doDeleteLabel = React.useCallback(
|
||||
async (id: backendModule.TagId, value: backendModule.LabelName) => {
|
||||
setDeletedLabelNames(oldNames => new Set([...oldNames, value]))
|
||||
setCurrentLabels(oldLabels => {
|
||||
let found = identity.identity<boolean>(false)
|
||||
const newLabels = oldLabels?.filter(oldLabel => {
|
||||
if (oldLabel === value) {
|
||||
found = true
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
return newLabels != null && newLabels.length > 0
|
||||
? found
|
||||
? newLabels
|
||||
: oldLabels
|
||||
: null
|
||||
})
|
||||
setQuery(oldQuery => oldQuery.delete({ labels: [value] }))
|
||||
try {
|
||||
await backend.deleteTag(id, value)
|
||||
dispatchAssetEvent({
|
||||
@ -260,6 +222,7 @@ export default function Drive(props: DriveProps) {
|
||||
},
|
||||
[
|
||||
backend,
|
||||
/* should never change */ setQuery,
|
||||
/* should never change */ dispatchAssetEvent,
|
||||
/* should never change */ toastAndLog,
|
||||
]
|
||||
@ -321,14 +284,14 @@ export default function Drive(props: DriveProps) {
|
||||
<div className="flex flex-col gap-4 text-base text-center">
|
||||
Upgrade your plan to use {common.PRODUCT_NAME} Cloud.
|
||||
<a
|
||||
className="block self-center whitespace-nowrap text-base text-white bg-help rounded-full self-center leading-170 h-8 py-px px-2 w-min"
|
||||
className="block self-center whitespace-nowrap text-base text-white bg-help rounded-full leading-170 h-8 py-px px-2 w-min"
|
||||
href="https://enso.org/pricing"
|
||||
>
|
||||
Upgrade
|
||||
</a>
|
||||
{!supportsLocalBackend && (
|
||||
<button
|
||||
className="block self-center whitespace-nowrap text-base text-white bg-help rounded-full self-center leading-170 h-8 py-px px-2 w-min"
|
||||
className="block self-center whitespace-nowrap text-base text-white bg-help rounded-full leading-170 h-8 py-px px-2 w-min"
|
||||
onClick={async () => {
|
||||
const downloadUrl = await appInfo.getDownloadUrl()
|
||||
if (downloadUrl == null) {
|
||||
@ -375,8 +338,8 @@ export default function Drive(props: DriveProps) {
|
||||
/>
|
||||
<Labels
|
||||
labels={labels}
|
||||
currentLabels={currentLabels}
|
||||
setCurrentLabels={setCurrentLabels}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
doCreateLabel={doCreateLabel}
|
||||
doDeleteLabel={doDeleteLabel}
|
||||
newLabelNames={newLabelNames}
|
||||
@ -386,9 +349,9 @@ export default function Drive(props: DriveProps) {
|
||||
)}
|
||||
<AssetsTable
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
category={category}
|
||||
allLabels={allLabels}
|
||||
currentLabels={currentLabels}
|
||||
initialProjectName={initialProjectName}
|
||||
projectStartupInfo={projectStartupInfo}
|
||||
deletedLabelNames={deletedLabelNames}
|
||||
|
@ -47,6 +47,7 @@ export default function Label(props: InternalLabelProps) {
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
title="Right click to remove label."
|
||||
className={`flex items-center rounded-full gap-1.5 h-6 px-2.25 ${className} ${
|
||||
active ? '' : 'text-not-selected opacity-50'
|
||||
} ${disabled ? '' : 'group-hover:opacity-100'} ${
|
||||
|
@ -4,6 +4,7 @@ import * as React from 'react'
|
||||
import PlusIcon from 'enso-assets/plus.svg'
|
||||
import Trash2Icon from 'enso-assets/trash2.svg'
|
||||
|
||||
import type * as assetQuery from '../../assetQuery'
|
||||
import type * as backend from '../backend'
|
||||
import * as drag from '../drag'
|
||||
import * as modalProvider from '../../providers/modal'
|
||||
@ -21,8 +22,8 @@ import SvgMask from '../../authentication/components/svgMask'
|
||||
/** Props for a {@link Labels}. */
|
||||
export interface LabelsProps {
|
||||
labels: backend.Label[]
|
||||
currentLabels: backend.LabelName[] | null
|
||||
setCurrentLabels: React.Dispatch<React.SetStateAction<backend.LabelName[] | null>>
|
||||
query: assetQuery.AssetQuery
|
||||
setQuery: React.Dispatch<React.SetStateAction<assetQuery.AssetQuery>>
|
||||
doCreateLabel: (name: string, color: backend.LChColor) => void
|
||||
doDeleteLabel: (id: backend.TagId, name: backend.LabelName) => void
|
||||
newLabelNames: Set<backend.LabelName>
|
||||
@ -33,13 +34,14 @@ export interface LabelsProps {
|
||||
export default function Labels(props: LabelsProps) {
|
||||
const {
|
||||
labels,
|
||||
currentLabels,
|
||||
setCurrentLabels,
|
||||
query,
|
||||
setQuery,
|
||||
doCreateLabel,
|
||||
doDeleteLabel,
|
||||
newLabelNames,
|
||||
deletedLabelNames,
|
||||
} = props
|
||||
const currentLabels = query.labels
|
||||
const { setModal } = modalProvider.useSetModal()
|
||||
|
||||
return (
|
||||
@ -57,21 +59,14 @@ export default function Labels(props: LabelsProps) {
|
||||
<Label
|
||||
draggable
|
||||
color={label.color}
|
||||
active={currentLabels?.includes(label.value) ?? false}
|
||||
active={currentLabels.includes(label.value)}
|
||||
disabled={newLabelNames.has(label.value)}
|
||||
onClick={() => {
|
||||
setCurrentLabels(oldLabels => {
|
||||
if (oldLabels == null) {
|
||||
return [label.value]
|
||||
} else {
|
||||
const newLabels = oldLabels.includes(label.value)
|
||||
? oldLabels.filter(
|
||||
oldLabel => oldLabel !== label.value
|
||||
)
|
||||
: [...oldLabels, label.value]
|
||||
return newLabels.length === 0 ? null : newLabels
|
||||
}
|
||||
})
|
||||
setQuery(oldQuery =>
|
||||
oldQuery.labels.includes(label.value)
|
||||
? oldQuery.delete({ labels: [label.value] })
|
||||
: oldQuery.add({ labels: [label.value] })
|
||||
)
|
||||
}}
|
||||
onDragStart={event => {
|
||||
drag.setDragImageToBlank(event)
|
||||
|
@ -75,6 +75,7 @@ export default function NewLabelModal(props: NewLabelModalProps) {
|
||||
<div className="w-12 h-6 py-1">Name</div>
|
||||
<input
|
||||
autoFocus
|
||||
size={1}
|
||||
placeholder="Enter the name of the label"
|
||||
className={`grow bg-transparent border border-black-a10 rounded-full leading-170 h-6 px-4 py-px ${
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
||||
|
||||
import FindIcon from 'enso-assets/find.svg'
|
||||
|
||||
import * as assetQuery from '../../assetQuery'
|
||||
import type * as backendModule from '../backend'
|
||||
import * as shortcuts from '../shortcuts'
|
||||
|
||||
@ -28,8 +29,8 @@ export interface TopBarProps {
|
||||
setBackendType: (backendType: backendModule.BackendType) => void
|
||||
isHelpChatOpen: boolean
|
||||
setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
|
||||
query: string
|
||||
setQuery: (value: string) => void
|
||||
query: assetQuery.AssetQuery
|
||||
setQuery: (query: assetQuery.AssetQuery) => void
|
||||
doRemoveSelf: () => void
|
||||
onSignOut: () => void
|
||||
}
|
||||
@ -95,9 +96,9 @@ export default function TopBar(props: TopBarProps) {
|
||||
size={1}
|
||||
id="search"
|
||||
placeholder="Type to search for projects, data connectors, users, and more."
|
||||
value={query}
|
||||
value={query.query}
|
||||
onChange={event => {
|
||||
setQuery(event.target.value)
|
||||
setQuery(assetQuery.AssetQuery.fromString(event.target.value))
|
||||
}}
|
||||
className="grow bg-transparent leading-5 h-6 py-px"
|
||||
/>
|
||||
|
@ -3,6 +3,8 @@ import * as authentication from 'enso-authentication'
|
||||
|
||||
import * as detect from 'enso-common/src/detect'
|
||||
|
||||
import './tailwind.css'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
@ -19,7 +21,7 @@ const SERVICE_WORKER_PATH = './serviceWorker.js'
|
||||
// === Live reload ===
|
||||
// ===================
|
||||
|
||||
if (detect.IS_DEV_MODE) {
|
||||
if (detect.IS_DEV_MODE && (!(typeof IS_VITE !== 'undefined') || !IS_VITE)) {
|
||||
new EventSource(ESBUILD_PATH).addEventListener(ESBUILD_EVENT_NAME, () => {
|
||||
// This acts like `location.reload`, but it preserves the query-string.
|
||||
// The `toString()` is to bypass a lint without using a comment.
|
||||
|
148
app/ide-desktop/lib/dashboard/tailwind.config.js
Normal file
148
app/ide-desktop/lib/dashboard/tailwind.config.js
Normal file
@ -0,0 +1,148 @@
|
||||
/** @file Configuration for Tailwind. */
|
||||
|
||||
// The names come from a third-party API and cannot be changed.
|
||||
/* eslint-disable no-restricted-syntax, @typescript-eslint/naming-convention, @typescript-eslint/no-magic-numbers */
|
||||
export default /** @satisfies {import('tailwindcss').Config} */ ({
|
||||
content: ['./src/**/*.tsx', './src/**/*.ts'],
|
||||
important: `:is(.enso-dashboard, .enso-chat)`,
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
/** The default color of all text. */
|
||||
// This should be named "regular".
|
||||
primary: 'rgba(0, 0, 0, 0.60)',
|
||||
'not-selected': 'rgba(0, 0, 0, 0.40)',
|
||||
'icon-selected': 'rgba(0, 0, 0, 0.50)',
|
||||
'icon-not-selected': 'rgba(0, 0, 0, 0.30)',
|
||||
chat: '#484848',
|
||||
'ide-bg': '#ebeef1',
|
||||
'ide-bg-dark': '#d0d3d6',
|
||||
selected: 'rgba(255, 255, 255, 0.40)',
|
||||
// Should be `#3e515f14`, but `bg-opacity` does not work with RGBA.
|
||||
label: '#f0f1f3',
|
||||
help: '#3f68ce',
|
||||
invite: '#0e81d4',
|
||||
cloud: '#0666be',
|
||||
share: '#64b526',
|
||||
inversed: '#ffffff',
|
||||
green: '#3e8b29',
|
||||
delete: 'rgba(243, 24, 10, 0.87)',
|
||||
v3: '#252423',
|
||||
youtube: '#c62421',
|
||||
discord: '#404796',
|
||||
dim: 'rgba(0, 0, 0, 0.25)',
|
||||
frame: 'rgba(255, 255, 255, 0.40)',
|
||||
'frame-selected': 'rgba(255, 255, 255, 0.70)',
|
||||
'tag-text': 'rgba(255, 255, 255, 0.90)',
|
||||
'tag-text-2': 'rgba(0, 0, 0, 0.60)',
|
||||
'permission-owner': 'rgba(236, 2, 2, 0.70)',
|
||||
'permission-admin': 'rgba(252, 60, 0, 0.70)',
|
||||
'permission-edit': 'rgba(255, 138, 0, 0.90)',
|
||||
'permission-read': 'rgba(152, 174, 18, 0.80)',
|
||||
'permission-docs': 'rgba(91, 8, 226, 0.64)',
|
||||
'permission-exec': 'rgba(236, 2, 2, 0.70)',
|
||||
'permission-view': 'rgba(0, 0, 0, 0.10)',
|
||||
'label-running-project': '#257fd2',
|
||||
'label-low-resources': '#ff6b18',
|
||||
'call-to-action': '#fa6c08',
|
||||
'black-a5': 'rgba(0, 0, 0, 0.05)',
|
||||
'black-a10': 'rgba(0, 0, 0, 0.10)',
|
||||
'black-a16': 'rgba(0, 0, 0, 0.16)',
|
||||
'black-a30': 'rgba(0, 0, 0, 0.30)',
|
||||
'black-a50': 'rgba(0, 0, 0, 0.50)',
|
||||
'gray-350': '#b7bcc5',
|
||||
},
|
||||
fontSize: {
|
||||
xs: '0.71875rem',
|
||||
sm: '0.8125rem',
|
||||
xl: '1.1875rem',
|
||||
'4xl': '2.375rem',
|
||||
},
|
||||
borderRadius: {
|
||||
'4xl': '2rem',
|
||||
},
|
||||
lineHeight: {
|
||||
144.5: '144.5%',
|
||||
170: '170%',
|
||||
},
|
||||
spacing: {
|
||||
0.75: '0.1875rem',
|
||||
1.25: '0.3125rem',
|
||||
1.75: '0.4375rem',
|
||||
2.25: '0.5625rem',
|
||||
3.25: '0.8125rem',
|
||||
3.5: '0.875rem',
|
||||
4.5: '1.125rem',
|
||||
4.75: '1.1875rem',
|
||||
5.5: '1.375rem',
|
||||
6.5: '1.625rem',
|
||||
9.5: '2.375rem',
|
||||
9.75: '2.4375rem',
|
||||
13: '3.25rem',
|
||||
18: '4.5rem',
|
||||
25: '6.25rem',
|
||||
29: '7.25rem',
|
||||
30: '7.5rem',
|
||||
30.25: '7.5625rem',
|
||||
42: '10.5rem',
|
||||
45: '11.25rem',
|
||||
51: '12.75rem',
|
||||
51.5: '12.875rem',
|
||||
54: '13.5rem',
|
||||
57.5: '14.375rem',
|
||||
62: '15.5rem',
|
||||
70: '17.5rem',
|
||||
83.5: '20.875rem',
|
||||
98.25: '24.5625rem',
|
||||
112.5: '28.125rem',
|
||||
115.25: '28.8125rem',
|
||||
140: '35rem',
|
||||
'10lh': '10lh',
|
||||
},
|
||||
minWidth: {
|
||||
31.5: '7.875rem',
|
||||
33.25: '8.3125rem',
|
||||
40: '10rem',
|
||||
61.25: '15.3125rem',
|
||||
80: '20rem',
|
||||
96: '24rem',
|
||||
},
|
||||
opacity: {
|
||||
'1/3': '.33333333',
|
||||
},
|
||||
zIndex: {
|
||||
1: '1',
|
||||
3: '3',
|
||||
},
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
},
|
||||
borderWidth: { 0.5: '0.5px' },
|
||||
boxShadow: {
|
||||
soft: `0 0.5px 2.2px 0px #00000008, 0 1.2px 5.3px 0px #0000000b, \
|
||||
0 2.3px 10px 0 #0000000e, 0 4px 18px 0 #00000011, 0 7.5px 33.4px 0 #00000014, \
|
||||
0 18px 80px 0 #0000001c`,
|
||||
},
|
||||
animation: {
|
||||
'spin-ease': 'spin cubic-bezier(0.67, 0.33, 0.33, 0.67) 1.5s infinite',
|
||||
},
|
||||
transitionProperty: {
|
||||
width: 'width',
|
||||
'stroke-dasharray': 'stroke-dasharray',
|
||||
'grid-template-rows': 'grid-template-rows',
|
||||
},
|
||||
transitionDuration: {
|
||||
5000: '5000ms',
|
||||
90000: '90000ms',
|
||||
},
|
||||
gridTemplateRows: {
|
||||
'0fr': '0fr',
|
||||
'1fr': '1fr',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'fill-60': 'repeat(auto-fill, minmax(15rem, 1fr))',
|
||||
'fill-75': 'repeat(auto-fill, minmax(18.75rem, 1fr))',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@ -1,158 +0,0 @@
|
||||
/** @file Configuration for Tailwind. */
|
||||
import * as path from 'node:path'
|
||||
import * as url from 'node:url'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
const THIS_PATH = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)))
|
||||
|
||||
// =====================
|
||||
// === Configuration ===
|
||||
// =====================
|
||||
|
||||
// The names come from a third-party API and cannot be changed.
|
||||
/* eslint-disable no-restricted-syntax, @typescript-eslint/naming-convention */
|
||||
export const content = [THIS_PATH + '/src/**/*.tsx', THIS_PATH + '/src/**/*.ts']
|
||||
export const important = `:is(.enso-dashboard, .enso-chat)`
|
||||
export const theme = {
|
||||
extend: {
|
||||
colors: {
|
||||
/** The default color of all text. */
|
||||
// This should be named "regular".
|
||||
primary: 'rgba(0, 0, 0, 0.60)',
|
||||
'not-selected': 'rgba(0, 0, 0, 0.40)',
|
||||
'icon-selected': 'rgba(0, 0, 0, 0.50)',
|
||||
'icon-not-selected': 'rgba(0, 0, 0, 0.30)',
|
||||
chat: '#484848',
|
||||
'ide-bg': '#ebeef1',
|
||||
'ide-bg-dark': '#d0d3d6',
|
||||
selected: 'rgba(255, 255, 255, 0.40)',
|
||||
// Should be `#3e515f14`, but `bg-opacity` does not work with RGBA.
|
||||
label: '#f0f1f3',
|
||||
help: '#3f68ce',
|
||||
invite: '#0e81d4',
|
||||
cloud: '#0666be',
|
||||
share: '#64b526',
|
||||
inversed: '#ffffff',
|
||||
green: '#3e8b29',
|
||||
delete: 'rgba(243, 24, 10, 0.87)',
|
||||
v3: '#252423',
|
||||
youtube: '#c62421',
|
||||
discord: '#404796',
|
||||
dim: 'rgba(0, 0, 0, 0.25)',
|
||||
frame: 'rgba(255, 255, 255, 0.40)',
|
||||
'frame-selected': 'rgba(255, 255, 255, 0.70)',
|
||||
'tag-text': 'rgba(255, 255, 255, 0.90)',
|
||||
'tag-text-2': 'rgba(0, 0, 0, 0.60)',
|
||||
'permission-owner': 'rgba(236, 2, 2, 0.70)',
|
||||
'permission-admin': 'rgba(252, 60, 0, 0.70)',
|
||||
'permission-edit': 'rgba(255, 138, 0, 0.90)',
|
||||
'permission-read': 'rgba(152, 174, 18, 0.80)',
|
||||
'permission-docs': 'rgba(91, 8, 226, 0.64)',
|
||||
'permission-exec': 'rgba(236, 2, 2, 0.70)',
|
||||
'permission-view': 'rgba(0, 0, 0, 0.10)',
|
||||
'label-running-project': '#257fd2',
|
||||
'label-low-resources': '#ff6b18',
|
||||
'call-to-action': '#fa6c08',
|
||||
'black-a5': 'rgba(0, 0, 0, 0.05)',
|
||||
'black-a10': 'rgba(0, 0, 0, 0.10)',
|
||||
'black-a16': 'rgba(0, 0, 0, 0.16)',
|
||||
'black-a30': 'rgba(0, 0, 0, 0.30)',
|
||||
'black-a50': 'rgba(0, 0, 0, 0.50)',
|
||||
'gray-350': '#b7bcc5',
|
||||
},
|
||||
fontSize: {
|
||||
xs: '0.71875rem',
|
||||
sm: '0.8125rem',
|
||||
xl: '1.1875rem',
|
||||
'4xl': '2.375rem',
|
||||
},
|
||||
borderRadius: {
|
||||
'4xl': '2rem',
|
||||
},
|
||||
lineHeight: {
|
||||
'144.5': '144.5%',
|
||||
'170': '170%',
|
||||
},
|
||||
spacing: {
|
||||
'0.75': '0.1875rem',
|
||||
'1.25': '0.3125rem',
|
||||
'1.75': '0.4375rem',
|
||||
'2.25': '0.5625rem',
|
||||
'3.25': '0.8125rem',
|
||||
'3.5': '0.875rem',
|
||||
'4.5': '1.125rem',
|
||||
'4.75': '1.1875rem',
|
||||
'5.5': '1.375rem',
|
||||
'6.5': '1.625rem',
|
||||
'9.5': '2.375rem',
|
||||
'9.75': '2.4375rem',
|
||||
'13': '3.25rem',
|
||||
'18': '4.5rem',
|
||||
'25': '6.25rem',
|
||||
'29': '7.25rem',
|
||||
'30': '7.5rem',
|
||||
'30.25': '7.5625rem',
|
||||
'42': '10.5rem',
|
||||
'45': '11.25rem',
|
||||
'51': '12.75rem',
|
||||
'51.5': '12.875rem',
|
||||
'54': '13.5rem',
|
||||
'57.5': '14.375rem',
|
||||
'62': '15.5rem',
|
||||
'70': '17.5rem',
|
||||
'83.5': '20.875rem',
|
||||
'98.25': '24.5625rem',
|
||||
'112.5': '28.125rem',
|
||||
'115.25': '28.8125rem',
|
||||
'140': '35rem',
|
||||
'10lh': '10lh',
|
||||
},
|
||||
minWidth: {
|
||||
'31.5': '7.875rem',
|
||||
'33.25': '8.3125rem',
|
||||
'40': '10rem',
|
||||
'61.25': '15.3125rem',
|
||||
'80': '20rem',
|
||||
'96': '24rem',
|
||||
},
|
||||
opacity: {
|
||||
'1/3': '.33333333',
|
||||
},
|
||||
zIndex: {
|
||||
'1': '1',
|
||||
'3': '3',
|
||||
},
|
||||
backdropBlur: {
|
||||
xs: '2px',
|
||||
},
|
||||
borderWidth: { '0.5': '0.5px' },
|
||||
boxShadow: {
|
||||
soft: `0 0.5px 2.2px 0px #00000008, 0 1.2px 5.3px 0px #0000000b, \
|
||||
0 2.3px 10px 0 #0000000e, 0 4px 18px 0 #00000011, 0 7.5px 33.4px 0 #00000014, \
|
||||
0 18px 80px 0 #0000001c`,
|
||||
},
|
||||
animation: {
|
||||
'spin-ease': 'spin cubic-bezier(0.67, 0.33, 0.33, 0.67) 1.5s infinite',
|
||||
},
|
||||
transitionProperty: {
|
||||
width: 'width',
|
||||
'stroke-dasharray': 'stroke-dasharray',
|
||||
'grid-template-rows': 'grid-template-rows',
|
||||
},
|
||||
transitionDuration: {
|
||||
'5000': '5000ms',
|
||||
'90000': '90000ms',
|
||||
},
|
||||
gridTemplateRows: {
|
||||
'0fr': '0fr',
|
||||
'1fr': '1fr',
|
||||
},
|
||||
gridTemplateColumns: {
|
||||
'fill-60': 'repeat(auto-fill, minmax(15rem, 1fr))',
|
||||
'fill-75': 'repeat(auto-fill, minmax(18.75rem, 1fr))',
|
||||
},
|
||||
},
|
||||
}
|
30
app/ide-desktop/lib/dashboard/vite.config.ts
Normal file
30
app/ide-desktop/lib/dashboard/vite.config.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/** @file Configuration for vite. */
|
||||
import * as vite from 'vite'
|
||||
import vitePluginYaml from '@modyfi/vite-plugin-yaml'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
const SERVER_PORT = 8080
|
||||
|
||||
// =====================
|
||||
// === Configuration ===
|
||||
// =====================
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
export default vite.defineConfig({
|
||||
server: { port: SERVER_PORT },
|
||||
plugins: [vitePluginYaml()],
|
||||
define: {
|
||||
IS_VITE: JSON.stringify(true),
|
||||
REDIRECT_OVERRIDE: JSON.stringify(`http://localhost:${SERVER_PORT}`),
|
||||
CLOUD_ENV:
|
||||
process.env.ENSO_CLOUD_ENV != null
|
||||
? JSON.stringify(process.env.ENSO_CLOUD_ENV)
|
||||
: 'undefined',
|
||||
// Single hardcoded usage of `global` in by aws-amplify.
|
||||
'global.TYPED_ARRAY_SUPPORT': JSON.stringify(true),
|
||||
},
|
||||
})
|
2
app/ide-desktop/lib/types/globals.d.ts
vendored
2
app/ide-desktop/lib/types/globals.d.ts
vendored
@ -91,10 +91,10 @@ declare global {
|
||||
const BUNDLED_ENGINE_VERSION: string
|
||||
const BUILD_INFO: buildJson.BuildInfo
|
||||
const PROJECT_MANAGER_IN_BUNDLE_PATH: string
|
||||
const IS_DEV_MODE: boolean
|
||||
// This will be `undefined` when it is not defined by esbuild.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const REDIRECT_OVERRIDE: string | undefined
|
||||
const IS_VITE: boolean
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const CLOUD_ENV: 'npekin' | 'pbuchu' | 'production' | undefined
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
44
package-lock.json
generated
44
package-lock.json
generated
@ -249,10 +249,12 @@
|
||||
"eslint-plugin-jsdoc": "^46.8.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"playwright": "^1.38.0",
|
||||
"postcss": "^8.4.29",
|
||||
"react-toastify": "^9.1.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"tsx": "^3.12.6",
|
||||
"typescript": "~5.2.2"
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-x64": "^0.17.15",
|
||||
@ -2535,21 +2537,6 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "13.23.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
|
||||
"integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"dev": true,
|
||||
@ -6092,7 +6079,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001535",
|
||||
"version": "1.0.30001565",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
|
||||
"integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -6107,8 +6096,7 @@
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
]
|
||||
},
|
||||
"node_modules/capital-case": {
|
||||
"version": "1.0.4",
|
||||
@ -8796,21 +8784,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/globals": {
|
||||
"version": "13.23.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
|
||||
"integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
@ -13397,6 +13370,8 @@
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.29",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
|
||||
"integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -13411,7 +13386,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user