Save scripts to custom locations (use import/export buttons)#4258
Save scripts to custom locations (use import/export buttons)#4258sergeyteleshev wants to merge 29 commits intodevelfrom
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
TIP This summary will be updated as you push new changes. Give us feedback
…mportexport-buttons
| @@ -0,0 +1,17 @@ | |||
| /* | |||
| * CloudBeaver - Cloud Database Manager | |||
| * Copyright (C) 2020-2025 DBeaver Corp and others | |||
| * you may not use this file except in compliance with the License. | ||
| */ | ||
|
|
||
| .tabList { |
There was a problem hiding this comment.
Please use tailwind as amount of styles and its logic is small
| const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true }); | ||
| const notificationService = useService(NotificationService); | ||
|
|
||
| const state = useObservableRef<State>( |
There was a problem hiding this comment.
Its better not to use useObservableRef for such simple cases. You can use native react state so we can reuse this logic across code bases and in desktop env.
| const name = getSqlEditorName(state, dataSource, connection); | ||
| const script = dataSource.script; | ||
| const hasOnlyLocalExport = | ||
| this.scriptExportService.tabsContainer.has(LOCAL_EXPORT_TAB_ID) && this.scriptExportService.tabsContainer.getIdList().length === 1; |
There was a problem hiding this comment.
getDisplayed instead of getIdList, as tabs might be hidden
| const [footerNode, setFooterNode] = useState<HTMLDivElement | null>(null); | ||
| const footerRef = useCallback((node: HTMLDivElement | null) => setFooterNode(node), []); | ||
|
|
||
| const FooterSlot: React.FC<React.PropsWithChildren> = useCallback( |
There was a problem hiding this comment.
footerNode and FooterSlot seems very strange, could you please explain what are we trying to achive with this?
There was a problem hiding this comment.
The dialog uses a tab-based architecture where:
- Different export destinations are shown as tabs (
Local Download- CE,Cloud Storage- EE, etc.) - Each tab has different validation requirements and submit logic
- Local: just needs a filename
- Cloud Storage: needs filename + folder selection + async upload
The issue:
- The dialog can't know each tab's specific validation rules
- The dialog can't know each tab's specific submit logic
- But the dialog needs to render the buttons
The initial solution was the portal pattern. I just moved the whole logic inside tabs for simplicity and then teleported the buttons to their original position, so the layout is not broken and looks fine (CommonDialog demands quite a strict order of the elements - body & footer)
I pushed my second proposal for how to handle this issue using MobX state, React contexts & useEffect. But as for me, it looks fragile to have all handled by useEffect. It can cause an unexpected state during saving & can lead to a memory leak and an app freeze. This is why I first came up with the portal pattern. It seems more stable, but tricky, I agree
The source issue is more general, and we need to design a confident API for these case scenarios. It's just currently out of the scope of this task, so I propose 1 of these 2 solutions for the current task
| {...payload} | ||
| > | ||
| <CommonDialogBody noBodyPadding noOverflow> | ||
| <TabList className={s(style, { tabList: true })} underline /> |
There was a problem hiding this comment.
can we use tailwind here instead?
I thought useS and s are kinda deprecated. Am I wrong?
| if (!footerNode) { | ||
| return null; | ||
| } | ||
| return createPortal(children, footerNode); |
There was a problem hiding this comment.
I see why you need this here, but it looks a bit weird:
- Footer buttons are actually the same, but we duplicate them in different tabs
- Only onSubmit is different
- It is not a Tab responsibility to render buttons of a dialog
maybe we could pass actions in IScriptExportDialogContext?
{
canSubmit: boolean;
onSubmit: () => void;
loading?: boolean;
}
So the tab could do
action.onSubmit = () => {downloadSql(fileName, script)}
I see that you have tried to use export controller, but it was using react state. Can you try with mobx observable via useObservableRef?
There was a problem hiding this comment.
webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportTab.tsx
Outdated
Show resolved
Hide resolved
…mportexport-buttons
…mportexport-buttons
webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportTab.tsx
Outdated
Show resolved
Hide resolved
webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportTab.tsx
Outdated
Show resolved
Hide resolved
webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportTab.tsx
Outdated
Show resolved
Hide resolved
webapp/packages/plugin-script-export/src/ExportScriptDialog/ScriptExportDialogContext.ts
Outdated
Show resolved
Hide resolved
webapp/packages/plugin-script-export/src/ExportScriptDialog/ExportScriptDialog.tsx
Outdated
Show resolved
Hide resolved
webapp/packages/plugin-sql-editor/src/LocalExport/LocalExportTab.tsx
Outdated
Show resolved
Hide resolved
| className, | ||
| }) { | ||
| const scriptExportService = useService(ScriptExportService); | ||
| const [selectedTabId, setSelectedTabId] = useState<string | undefined>(undefined); |
There was a problem hiding this comment.
Seems like you are using this state just to pass to TabsState, you can omit it and keep only container
| const [fileName, setFileName] = useState(initialFileName); | ||
|
|
||
| const form = useForm({ | ||
| onSubmit(event) { |
| <Form ref={focusedRef} context={form}> | ||
| <InputField name="fileName" value={fileName} required small onChange={setFileName}> | ||
| <Translate token="ui_file_name" /> | ||
| </InputField> | ||
| </Form> |
There was a problem hiding this comment.
i think that use of <Form> component is optional here
closes https://github.com/dbeaver/pro/issues/7880