Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions web/client/src/context/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface Dialect {

interface EditorStore {
storedTabsIds: ID[]
tabs: Map<ID, EditorTab>
tabs: Map<ModelFile, EditorTab>
tab: EditorTab
engine: Worker
dialects: Dialect[]
Expand All @@ -20,7 +20,7 @@ interface EditorStore {
previewConsole?: string
selectTab: (tab: EditorTab) => void
addTab: (tab: EditorTab) => void
closeTab: (id: ID) => void
closeTab: (file: ModelFile) => void
createTab: (file?: ModelFile) => EditorTab
setDialects: (dialects: Dialect[]) => void
refreshTab: () => void
Expand Down Expand Up @@ -48,7 +48,7 @@ const [getStoredTabs, setStoredTabs] = useLocalStorage<{ ids: ID[] }>('tabs')

const initialFile = createLocalFile()
const initialTab: EditorTab = createTab(initialFile, true)
const initialTabs = new Map([[initialFile.id, initialTab]])
const initialTabs = new Map([[initialFile, initialTab]])

export const useStoreEditor = create<EditorStore>((set, get) => ({
storedTabsIds: getStoredTabsIds(),
Expand All @@ -70,7 +70,7 @@ export const useStoreEditor = create<EditorStore>((set, get) => ({
get().addTab(tab)
},
addTab(tab) {
const tabs = new Map([...get().tabs, [tab.file.id, tab]])
const tabs = new Map([...get().tabs, [tab.file, tab]])

setStoredTabs({
ids: Array.from(tabs.values())
Expand All @@ -83,17 +83,17 @@ export const useStoreEditor = create<EditorStore>((set, get) => ({
tabs,
}))
},
closeTab(id) {
closeTab(file) {
const s = get()

if (isTrue(s.tabs.get(id)?.isInitial)) return
if (isTrue(s.tabs.get(file)?.isInitial)) return

const tabs = Array.from(get().tabs.values())
const indexAt = tabs.findIndex(tab => tab.file.id === id)
const indexAt = tabs.findIndex(tab => tab.file === file)

s.tabs.delete(id)
s.tabs.delete(file)

if (id === s.tab.file.id) {
if (file.id === s.tab.file.id) {
s.selectTab(tabs.at(indexAt - 1) as EditorTab)
}

Expand Down
2 changes: 1 addition & 1 deletion web/client/src/context/fileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ interface FileTreeStore {
selectedFile?: ModelFile
selectFile: (selectedFile: ModelFile) => void
setFiles: (files: ModelFile[]) => void
refreshProject: () => void
setProject: (project?: Directory) => void
refreshProject: () => void
}

export const useStoreFileTree = create<FileTreeStore>((set, get) => ({
Expand Down
2 changes: 1 addition & 1 deletion web/client/src/library/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function Editor(): JSX.Element {
}, [tab])

useEffect(() => {
if (selectedFile == null) return
if (selectedFile == null || tab.file === selectedFile) return

selectTab(createTab(selectedFile))
}, [selectedFile])
Expand Down
4 changes: 2 additions & 2 deletions web/client/src/library/components/editor/EditorCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ export default function CodeEditor(): JSX.Element {
key: 'Mod-Alt-]',
preventDefault: true,
run() {
closeTab(tab.file.id)
closeTab(tab.file)

return true
},
},
],
[closeTab, selectTab, createTab, tab.file.id],
[closeTab, selectTab, createTab, tab.file],
)

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion web/client/src/library/components/editor/EditorTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function Tab({
const closeTab = useStoreEditor(s => s.closeTab)

function closeEditorTab(tab: EditorTab): void {
closeTab(tab.file.id)
closeTab(tab.file)
}

return (
Expand Down
5 changes: 2 additions & 3 deletions web/client/src/library/components/fileTree/Directory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default function Directory({

setIsLoading(true)

const extension = '.py'
const extension = directory.isModels ? '.sql' : '.py'
const name = toUniqueName('new_file', extension)

writeFileApiFilesPathPost(`${directory.path}/${name}`, { content: '' })
Expand All @@ -109,7 +109,6 @@ export default function Directory({
}

directory.addFile(new ModelFile(created, directory))

directory.open()
})
.catch(error => {
Expand Down Expand Up @@ -141,7 +140,7 @@ export default function Directory({
const files = getAllFilesInDirectory(directory)

files.forEach(file => {
closeTab(file.id)
closeTab(file)
})
}

Expand Down
174 changes: 113 additions & 61 deletions web/client/src/library/components/fileTree/File.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, type MouseEvent } from 'react'
import { useState, type MouseEvent, useEffect } from 'react'
import {
DocumentIcon,
XCircleIcon,
Expand All @@ -19,8 +19,6 @@ interface PropsFile extends WithConfirmation {
file: ModelFile
}

const CSS_ICON_SIZE = 'w-4 h-4'

export default function File({
file,
setConfirmation,
Expand All @@ -29,12 +27,17 @@ export default function File({
const tabs = useStoreEditor(s => s.tabs)
const closeTab = useStoreEditor(s => s.closeTab)

const selectedFile = useStoreFileTree(s => s.selectedFile)
const selectFile = useStoreFileTree(s => s.selectFile)
const refreshProject = useStoreFileTree(s => s.refreshProject)

const [isLoading, setIsLoading] = useState(false)
const [newName, setNewName] = useState<string>()

useEffect(() => {
selectFile(tab.file)
}, [tab])

function remove(): void {
if (isLoading) return

Expand All @@ -43,7 +46,7 @@ export default function File({
deleteFileApiFilesPathDelete(file.path)
.then(response => {
if ((response as unknown as { ok: boolean }).ok) {
closeTab(file.id)
closeTab(file)

file.parent?.removeFile(file)

Expand Down Expand Up @@ -107,14 +110,14 @@ export default function File({
<span
className={clsx(
'whitespace-nowrap group/file pl-3 pr-2 py-[0.125rem] flex rounded-md',
'hover:bg-neutral-100 dark:hover:bg-dark-lighter ',
'hover:bg-neutral-100 dark:hover:bg-dark-lighter',
file.is_supported &&
'group hover:bg-neutral-100 dark:hover:bg-dark-lighter',
isFalse(isStringEmptyOrNil(newName)) && 'bg-primary-800',
tabs.has(file.id)
tabs.has(file)
? 'text-brand-500'
: 'text-neutral-500 dark:text-neutral-100',
file === tab.file && 'bg-neutral-100 dark:bg-dark-lighter',
file === selectedFile && 'bg-neutral-100 dark:bg-dark-lighter',
)}
>
<span
Expand All @@ -125,71 +128,120 @@ export default function File({
<div className="flex items-center">
<DocumentIcon
className={clsx(
`inline-block ${CSS_ICON_SIZE} mr-2`,
file === tab.file
`inline-block w-4 h-4 mr-2`,
file === selectedFile
? 'text-brand-500'
: 'text-neutral-500 dark:text-neutral-100',
)}
/>
</div>
{isStringEmptyOrNil(newName) ? (
<>
<span
onClick={(e: MouseEvent) => {
e.stopPropagation()

file.is_supported && file !== tab.file && selectFile(file)
}}
onDoubleClick={(e: MouseEvent) => {
e.stopPropagation()

setNewName(file.name)
}}
className={clsx(
'w-full overflow-hidden overflow-ellipsis cursor-default',
!file.is_supported && 'opacity-50 cursor-not-allowed',
)}
>
{file.name}
</span>
<span
className="flex items-center invisible group-hover/file:visible min-w-8"
onClick={(e: MouseEvent) => {
e.stopPropagation()

removeWithConfirmation()
}}
>
<XCircleIcon
className={`inline-block ${CSS_ICON_SIZE} ml-2 text-danger-500 cursor-pointer`}
/>
</span>
<FileName
file={file}
setNewName={setNewName}
/>
<FileActions removeWithConfirmation={removeWithConfirmation} />
</>
) : (
<div className="w-full flex items-center">
<input
type="text"
className="w-full overflow-hidden overflow-ellipsis bg-primary-900 text-primary-100"
value={newName === '' ? file.name : newName}
onInput={(e: any) => {
e.stopPropagation()

setNewName(e.target.value)
}}
/>
<div className="flex">
<CheckCircleIcon
className={`inline-block ${CSS_ICON_SIZE} ml-2 text-success-500 cursor-pointer`}
onClick={(e: MouseEvent) => {
e.stopPropagation()

rename()
}}
/>
</div>
</div>
<FileRename
file={file}
newName={newName}
setNewName={setNewName}
rename={rename}
/>
)}
</span>
</span>
)
}

function FileName({
file,
setNewName,
}: {
file: ModelFile
setNewName: (name: string) => void
}): JSX.Element {
const selectedFile = useStoreFileTree(s => s.selectedFile)
const selectFile = useStoreFileTree(s => s.selectFile)

return (
<span
onClick={(e: MouseEvent) => {
e.stopPropagation()

file.is_supported && file !== selectedFile && selectFile(file)
}}
onDoubleClick={(e: MouseEvent) => {
e.stopPropagation()

setNewName(file.name)
}}
className={clsx(
'w-full overflow-hidden overflow-ellipsis cursor-default',
!file.is_supported && 'opacity-50 cursor-not-allowed',
)}
>
{file.name}
</span>
)
}

function FileRename({
file,
newName,
setNewName,
rename,
}: {
file: ModelFile
newName?: string
setNewName: (name: string) => void
rename: () => void
}): JSX.Element {
return (
<div className="w-full flex items-center">
<input
type="text"
className="w-full overflow-hidden overflow-ellipsis bg-primary-900 text-primary-100"
value={newName === '' ? file.name : newName}
onInput={(e: any) => {
e.stopPropagation()

setNewName(e.target.value)
}}
/>
<div className="flex">
<CheckCircleIcon
className={`inline-block w-4 h-4 ml-2 text-success-500 cursor-pointer`}
onClick={(e: MouseEvent) => {
e.stopPropagation()

rename()
}}
/>
</div>
</div>
)
}

function FileActions({
removeWithConfirmation,
}: {
removeWithConfirmation: () => void
}): JSX.Element {
return (
<span
className="flex items-center invisible group-hover/file:visible min-w-8"
onClick={(e: MouseEvent) => {
e.stopPropagation()

removeWithConfirmation()
}}
>
<XCircleIcon
className={`inline-block w-4 h-4 ml-2 text-danger-500 cursor-pointer`}
/>
</span>
)
}
4 changes: 4 additions & 0 deletions web/client/src/models/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export class ModelDirectory extends ModelArtifact<InitialDirectory> {
return !this.isOpen && this.allDirectories.every(d => d.isCollapsed)
}

get isModels(): boolean {
return this.path.startsWith('models')
}

open(): void {
this._isOpen = true

Expand Down