From e72f2c888335a5d6008c8b0d2c8fab1f1be071d2 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Tue, 25 May 2021 22:21:22 +0300 Subject: [PATCH 1/2] feat(plugin-data-spreadsheet-new): CB-666 grid context filter --- .../packages/core-app/public/icons/erase.png | Bin 0 -> 1055 bytes .../packages/core-app/public/icons/filter.png | Bin 0 -> 546 bytes .../public/icons/filter_clipboard.png | Bin 0 -> 238 bytes .../core-app/public/icons/filter_custom.png | Bin 0 -> 246 bytes .../core-app/public/icons/filter_reset.png | Bin 0 -> 859 bytes .../core-app/public/icons/filter_value.png | Bin 0 -> 229 bytes .../core-app/public/icons/permissions.png | Bin 0 -> 1091 bytes .../packages/core-bootstrap/src/manifest.ts | 3 + .../core-localization/src/locales/en.ts | 3 + .../core-localization/src/locales/ru.ts | 3 + .../queries/grid/getSqlExecuteTaskResults.gql | 6 + .../src/queries/grid/updateResultsData.gql | 1 + .../queries/grid/updateResultsDataBatch.gql | 1 + webapp/packages/core-sdk/src/sdk.ts | 37 +- .../src/Clipboard/ClipboardBootstrap.ts | 50 ++ .../src/Clipboard/ClipboardService.ts | 63 +++ .../src/{ => Clipboard}/copyToClipboard.ts | 0 webapp/packages/core-utils/src/index.ts | 6 +- .../packages/core-utils/src/replaceMiddle.ts | 15 + .../DataGridContextMenuFilterService.ts | 469 ++++++++++++++++++ .../FilterCustomValueDialog.tsx | 100 ++++ .../DataGridContextMenuService.ts | 15 +- .../DataGridContextMenuSortingService.ts | 29 +- .../src/DataGrid/Formatters/Menu/CellMenu.tsx | 2 +- .../DataGrid/TableColumnHeader/SortButton.tsx | 8 +- .../src/SpreadsheetBootstrap.ts | 3 + .../src/locales/en.ts | 7 + .../src/locales/ru.ts | 7 + .../src/manifest.ts | 2 + .../src/ContainerDataSource.ts | 2 + .../Actions/IDatabaseDataConstraintAction.ts | 33 ++ .../Actions/IDatabaseDataFormatAction.ts | 1 + .../Actions/IDatabaseDataSortAction.ts | 17 - .../ResultSet/ResultSetConstraintAction.ts | 272 ++++++++++ .../Actions/ResultSet/ResultSetDataAction.ts | 10 +- .../ResultSet/ResultSetFormatAction.ts | 10 + .../Actions/ResultSet/ResultSetSortAction.ts | 129 ----- .../DatabaseDataModel/DatabaseDataSource.ts | 3 +- .../DatabaseDataModel/IDatabaseDataSource.ts | 1 + .../TableHeader/TableWhereFilter.tsx | 34 +- .../packages/plugin-data-viewer/src/index.ts | 3 +- .../plugin-sql-editor/src/QueryDataSource.ts | 2 + 42 files changed, 1165 insertions(+), 182 deletions(-) create mode 100644 webapp/packages/core-app/public/icons/erase.png create mode 100644 webapp/packages/core-app/public/icons/filter.png create mode 100644 webapp/packages/core-app/public/icons/filter_clipboard.png create mode 100644 webapp/packages/core-app/public/icons/filter_custom.png create mode 100644 webapp/packages/core-app/public/icons/filter_reset.png create mode 100644 webapp/packages/core-app/public/icons/filter_value.png create mode 100644 webapp/packages/core-app/public/icons/permissions.png create mode 100644 webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts create mode 100644 webapp/packages/core-utils/src/Clipboard/ClipboardService.ts rename webapp/packages/core-utils/src/{ => Clipboard}/copyToClipboard.ts (100%) create mode 100644 webapp/packages/core-utils/src/replaceMiddle.ts create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts create mode 100644 webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts delete mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts delete mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetSortAction.ts diff --git a/webapp/packages/core-app/public/icons/erase.png b/webapp/packages/core-app/public/icons/erase.png new file mode 100644 index 0000000000000000000000000000000000000000..b69785aad764d9052d87cd1df0ba1f69214d9584 GIT binary patch literal 1055 zcmV+)1mOFLP)Px&*-1n}R9Fe^R!vM)R}?Nf~W2iRTx-nK-V-*Y7D%d79QQjDJ@78EyG)6(8^N!ygxHpgC^}P|< zGP4C9r^@o_dpjGehQsQ=HUR~GA(=W( zBA<>2| zvc`$qBiiPbmRHGjuE6c2b1V#8)kADO4^n6B=s?u3{wov6tUQ_eo`g2~74)--&_^^# zXZ%RK5`i)B4#>@ZK!G-)RrHL1&L)vF0R@h>AAyVq0tGsyfd~2Y(mu)4VG%tElU?uB zA3@C4u(%xjbBFKW<9ofw`c!OA9kAP+@35Vw0$lSm`zMq@tZkqj=X|@)oC?W8=J*E^ zh$WmP0CyNrrhQ25F#04X!q$&E)occB&=MQ2O?2L_sBty<5Vh(zir_QfV`%S8h0_sWVH4c@DHLI=?HZ*YAX zaJ2lO7hC~>5I9I2UE*9XB;<6@j?7%$**It13ps8N*a_!rfbZ6;f%A~lZ12;$S1J+% zO|i|8C%ksQITQB)yrFx?oEO=CLjbFZV^yE)0Jiwc8!h%;t@}wuqOPfW69CMnnh?j_ zTR5x47hJ^}0_4gblWfsfSlt&!516zvyLHXg8}X@5W{2U&kXOayXYMAQUA@ch|JvTs#E;*c< zJ(PB@;7{g#RRAk{0c{Qo4NoQpedl?@O5dlJv_hf4Pur;74t#QV2Z zJ=H$EBlvRR!)dWI*c09YZ|Xl=<~@L3i=yjrwAP|>HS`gDk`zdtz%@O3biGM$=l{13 ZyaX2QVV=cF@IL?m002ovPDHLkV1n2_04@Lk literal 0 HcmV?d00001 diff --git a/webapp/packages/core-app/public/icons/filter.png b/webapp/packages/core-app/public/icons/filter.png new file mode 100644 index 0000000000000000000000000000000000000000..955f59934e7115eb744de6fecf88cf2490e496db GIT binary patch literal 546 zcmV+-0^R+IP)Px$+(|@1R9Fe^md{H9K@`Vlc2}qjxf)(`=%FAgs6+k@{SoH1f1q2?KhdFc zhmLlT3>AZ*x`hRWEQDHOvaQoR^TE2ij&@6EjLhqrHLmyrK%)LwwZago`5 zVa*jv4hxy&ku{%Uos8nY<`ktBLf8`cd3nBOqLDnM)K^s+Eo0U+SqwEg3gNl?%?Y5g zO=^-XS6t>l%`l0Uiq9oDOr(FSc=l3jm2(gJ0Z z#pk6Vdg3v{$Q0~%d-mW5bM1jrI{Cy@vd!rpVNvtbSi2~{mUu-Bn2XGdIOK7y01DVE zG0iKnC#Zy1@u2{)Ko-J}xDuR_a%{CU>u|$TS?Tw~kT( zumKMr0RIZnZ?!4OVZGS);Q#;t07*qoM6N<$g5PWU9smFU literal 0 HcmV?d00001 diff --git a/webapp/packages/core-app/public/icons/filter_clipboard.png b/webapp/packages/core-app/public/icons/filter_clipboard.png new file mode 100644 index 0000000000000000000000000000000000000000..110b3824f79969ed1df5f95211fe189dba835850 GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`TRmMILo7}w|M>sko|z-L!SuX^ z(4L`WdbpjPtZG6NRBu_|N!7UNM)9^9y6H@@gGDb<|cJXr&4U!F_izORO l#X8s)+?esTLH-yk!zq5IiKX)@s(>C~@O1TaS?83{1OT{YR+j(( literal 0 HcmV?d00001 diff --git a/webapp/packages/core-app/public/icons/filter_custom.png b/webapp/packages/core-app/public/icons/filter_custom.png new file mode 100644 index 0000000000000000000000000000000000000000..3acf6afe0002e2708debf7aed9ef7a0eb92432f6 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dp%toLo7}w|M>sko>^7G>C628 z{zCK0oSPWgCh&ULd}PyalQ#G%XT6KDg7>5Sk4O5G((E==>98f3vaC+yVRm4ct#~}{ z|MAt5M;T0<9oSrCntwZ)J%(TA{``O6D{@i>bAS-P zhq0H_p(KVg0wv-HVjAx^tlZu*F<9=-H-S{M6y`dq&c`K;U5XNM3KmdKI;Vst0M@Kne*gdg literal 0 HcmV?d00001 diff --git a/webapp/packages/core-app/public/icons/filter_reset.png b/webapp/packages/core-app/public/icons/filter_reset.png new file mode 100644 index 0000000000000000000000000000000000000000..399bd24f5c84933433b378e7e8de2a3ec22ed01c GIT binary patch literal 859 zcmV-h1ElPx&6-h)vR9Fe^md#HSK@`B}?QRR&CLTZ`8bb?cK_#G2uml7X6ZGK0`1KcfFea!G zV~m$3-i)FbW5Qox;>koo4^nSfK+!ZN*76xb2mv`XAe8RzIB)4T(@mExOVNW&vfX(f z^WLwoZ)Qr!|2_ZqgAt3ucDDrI)lsK*k6q73*_I6uF&YK@eNHTlp0oP|=`+Nu&FlpbPFK|)p?9kr!?0q8oQ$zr2=Xms)8 zJeOP`!r8$aHPc2vUeNg z^=tVv&VR3|vvwK|*BXZ@06HQB5V)CMq)@$QxHAGC<3D)0O-pyCJk$hqLtm~ zpCMZUWO#3bumh0y3Pc;dt@vd7wW95Zi=%$;HQtt2fM`>Bi%Qg9m8yL|{oYf2qF8I? zUUfMUV)mYBljpu}%PRmSY6H90-7L6EVIZ-9rwtR0HdpME6Uh*p9z&U3*DW*{)N*eM z;KyuSArur0l5JFlC!Y~PxSO)MWSRCxPkOEjAR8tB0nZfIuGdHkLf(t1G(9Cr7(w1<+}T{j1?25=rGa>dnT4 z-&0CdxWd~hrH!$X#Q?9vc}@W6XoNsrwlUu1ElB|Br-b^@&hC;(%1>)d3iMzc_ZTvH%}m l$Ir*kl<(o?o%uhnz#m(rY1;CP<~slY002ovPDHLkV1fz`m1+P0 literal 0 HcmV?d00001 diff --git a/webapp/packages/core-app/public/icons/filter_value.png b/webapp/packages/core-app/public/icons/filter_value.png new file mode 100644 index 0000000000000000000000000000000000000000..261cbc484fd93168f24762796ac3e9ec501e59fb GIT binary patch literal 229 zcmV^P)IhXza-d z#aIQ(xa-5Bl)PY^j!~io*tkUitqDYlhv!i9=h$1sDx(rxj~!rHkR(c979a~USixIC zi2L(g3plt>z{DQ}x7W!7@5U9dguY<)C(31}Px&{YgYYR9Fe+R&PjCQ53)Ty|=lVn3bgln`zb-S_oQJMva1~Fd`%*CU7 zfLI}ORtjSY^2yc1-m^lYh+cUE!jqDUT^Z-(qxkxoCGm<5uqbndn|5pCz0RUl- zn4%&oD6cEd@mdsVx`?;+et!Es5L%5`nJwc{{d#+WIG#+t*!uq6<5%A+z^<{QuI#*M zZwMg6qU4wrnkgEYO_Go6v#biSXPWI?A5U+enKlG=jc^5z#M&DIh&7lBeci%Y4-T!d z8OYIh%Fh6$^u*F!>%l~cq-+I%m;q@W^v#LL5l_hq0I3STYdASnS<3d6nwZOFNLYUh zN}D<{TD_&2B#%TTbP#F3LuxKKnTLQPQ~wN$@+C4DM<~j%Ub(Qfu*K(^Z}}ad@WyES z!OW_gQtGFKHaOkBYp5Fn(2XYXfvit;)|ED3#$M0BeJJy5;<=M3t#`QJoSE7!pyW7` zBVn&+pf(XXjx)kedMh0B40}>+%WO5tBntQT|1ms=fFSxO#eX z{$&or`@4(Fu+I0>h_rK9p*9jy@4|j(!TTdqZMY(e2qzR~r!R>uME&1_kAs8ZW3f2L z`lq^Njw|9E1X%WCJ1C8i>tmG7hcxvjOLB8T`|FROeC#gIy9xMS9DpF_PI?QF9DZkY zCh^1QXRd0RR*5&Eue>mWG8#D(vPOPPDu9IKz${VH#sdKI{oWP%K9!C5VecIrCz7ed zumzF9B*Iwa-Q@*U8d0PEco4?&B6l5p!}YuFXz%q-Xa$my6QbULeb$o$B9Rx)3sHgn zk|Qs@26GG>0f?s(fwsPZ=Zd74QB?Kzbtsh0R<(5H4To4Y>{ym)pv#o_p~77<-~hyu zL!JLaDBI+8CnraLkpSX3Plm;kb9;EqY8`H0+<>_a?2$y93>;%sm8RH zp7Q(yTpz2SZ~%;{8~9ZNf?u*Mt%vf{#K|;>j3;wG=Py>}QO8YQOIZK_002ov JPDHLkV1fzi4SxUt literal 0 HcmV?d00001 diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 0d540b85e8..40c21578e9 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -95,6 +95,7 @@ import { EnvironmentService, GraphQLService } from '@cloudbeaver/core-sdk'; import { LocalStorageSaveService, SettingsService } from '@cloudbeaver/core-settings'; import { ThemeService } from '@cloudbeaver/core-theming'; import { NavigationService, OptionsPanelService } from '@cloudbeaver/core-ui'; +import { ClipboardBootstrap, ClipboardService } from '@cloudbeaver/core-utils'; import { ActiveViewService } from '@cloudbeaver/core-view'; export const coreManifest: PluginManifest = { @@ -135,6 +136,8 @@ export const coreManifest: PluginManifest = { PermissionsService, CoreSettingsService, CommonDialogService, + ClipboardService, + ClipboardBootstrap, SessionExpireService, SessionExpiredDialogService, ConnectionsLocaleService, diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 61691e047a..355ee5e076 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -25,9 +25,12 @@ export const defaultENLocale = [ ['ui_data_delete_confirmation', 'Delete confirmation'], ['ui_no_matches_placeholder', 'Your search returned no matches.'], ['ui_information', 'Information'], + ['ui_clipboard', 'Clipboard'], ['ui_copy_to_clipboard', 'Copy'], ['ui_copy_to_clipboard_copied', 'Copied'], ['ui_copy_to_clipboard_failed_to_copy', 'Failed to copy'], + ['ui_clipboard_access_denied_title', 'Clipboard access denied'], + ['ui_clipboard_access_denied_message', 'You need to give access to clipboard to use some of the app features'], ['root_permission_denied', 'You have no permissions'], ['app_root_session_expired_title', 'Session expired'], ['app_root_session_expired_message', 'Session has been expired. Do you want to reload?'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 5ad7af55af..87bd355056 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -23,9 +23,12 @@ export const defaultRULocale = [ ['ui_no_matches_placeholder', 'По вашему запросу ничего не найдено.'], ['ui_data_delete_confirmation', 'Подтвердите удаление'], ['ui_information', 'Информация'], + ['ui_clipboard', 'Буфер обмена'], ['ui_copy_to_clipboard', 'Копировать'], ['ui_copy_to_clipboard_copied', 'Скопировано'], ['ui_copy_to_clipboard_failed_to_copy', 'Не удалось скопировать'], + ['ui_clipboard_access_denied_title', 'Доступ к буферу обмена запрещен'], + ['ui_clipboard_access_denied_message', 'Чтобы использовать некоторые функции приложения, необходим доступ к буферу обмена'], ['root_permission_denied', 'Отказано в доступе'], ['app_root_session_expired_title', 'Сессия истекла'], ['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'], diff --git a/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql b/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql index 3fb6369daf..8400b4e10f 100644 --- a/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql +++ b/webapp/packages/core-sdk/src/queries/grid/getSqlExecuteTaskResults.gql @@ -6,6 +6,7 @@ mutation getSqlExecuteTaskResults( ) { duration statusMessage + filterText results { title updateRowCount @@ -26,6 +27,11 @@ mutation getSqlExecuteTaskResults( readOnly scale typeName + supportedOperations { + id + expression + argumentCount + } } rows hasMoreData diff --git a/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql b/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql index bb41a25bfc..25708872a5 100644 --- a/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql +++ b/webapp/packages/core-sdk/src/queries/grid/updateResultsData.gql @@ -15,6 +15,7 @@ mutation updateResultsData( updateValues: $values ) { duration + filterText results { updateRowCount resultSet { diff --git a/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql b/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql index d590a29d2a..eb49271b7b 100644 --- a/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql +++ b/webapp/packages/core-sdk/src/queries/grid/updateResultsDataBatch.gql @@ -16,6 +16,7 @@ mutation updateResultsDataBatch( addedRows: $addedRows ) { duration + filterText results { updateRowCount resultSet { diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index c23e10c827..cf76949baf 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -65,6 +65,7 @@ export interface Query { sqlCompletionProposals?: Maybe>>; sqlDialectInfo?: Maybe; sqlListContexts?: Maybe>>; + sqlSupportedOperations: DataTypeLogicalOperation[]; templateConnections: ConnectionInfo[]; updateConnectionConfiguration: ConnectionInfo; userConnections: ConnectionInfo[]; @@ -247,6 +248,13 @@ export interface QuerySqlListContextsArgs { connectionId: Scalars['ID']; } +export interface QuerySqlSupportedOperationsArgs { + connectionId: Scalars['ID']; + contextId: Scalars['ID']; + resultsId: Scalars['ID']; + attributeIndex: Scalars['Int']; +} + export interface QueryUpdateConnectionConfigurationArgs { id: Scalars['ID']; config: ConnectionConfig; @@ -810,6 +818,7 @@ export interface SqlResultColumn { precision?: Maybe; readOnly: Scalars['Boolean']; readOnlyStatus?: Maybe; + supportedOperations: DataTypeLogicalOperation[]; } export interface DatabaseDocument { @@ -836,7 +845,8 @@ export interface SqlQueryResults { export interface SqlExecuteInfo { statusMessage?: Maybe; - duration?: Maybe; + duration: Scalars['Int']; + filterText?: Maybe; results: SqlQueryResults[]; } @@ -845,6 +855,12 @@ export interface SqlResultRow { updateValues?: Maybe; } +export interface DataTypeLogicalOperation { + id: Scalars['ID']; + expression: Scalars['String']; + argumentCount?: Maybe; +} + export enum AdminSubjectType { User = 'user', Role = 'role' @@ -1423,12 +1439,15 @@ export type GetSqlExecuteTaskResultsMutationVariables = Exact<{ export interface GetSqlExecuteTaskResultsMutation { result: ( - Pick + Pick & { results: Array<( Pick & { resultSet?: Maybe<( Pick - & { columns?: Maybe>>> } + & { columns?: Maybe + & { supportedOperations: Array> } + )>>>; } )>; } )>; } ); @@ -1444,7 +1463,7 @@ export type UpdateResultsDataMutationVariables = Exact<{ export interface UpdateResultsDataMutation { result?: Maybe<( - Pick + Pick & { results: Array<( Pick & { resultSet?: Maybe> } @@ -1463,7 +1482,7 @@ export type UpdateResultsDataBatchMutationVariables = Exact<{ export interface UpdateResultsDataBatchMutation { result?: Maybe<( - Pick + Pick & { results: Array<( Pick & { resultSet?: Maybe> } @@ -2308,6 +2327,7 @@ export const GetSqlExecuteTaskResultsDocument = ` result: asyncSqlExecuteResults(taskId: $taskId) { duration statusMessage + filterText results { title updateRowCount @@ -2328,6 +2348,11 @@ export const GetSqlExecuteTaskResultsDocument = ` readOnly scale typeName + supportedOperations { + id + expression + argumentCount + } } rows hasMoreData @@ -2346,6 +2371,7 @@ export const UpdateResultsDataDocument = ` updateValues: $values ) { duration + filterText results { updateRowCount resultSet { @@ -2367,6 +2393,7 @@ export const UpdateResultsDataBatchDocument = ` addedRows: $addedRows ) { duration + filterText results { updateRowCount resultSet { diff --git a/webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts b/webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts new file mode 100644 index 0000000000..e329b3cdff --- /dev/null +++ b/webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts @@ -0,0 +1,50 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { ClipboardService } from './ClipboardService'; + +@injectable() +export class ClipboardBootstrap extends Bootstrap { + constructor( + private clipboardService: ClipboardService, + ) { + super(); + } + + register(): void { } + + async load(): Promise { + if (!navigator.clipboard) { + this.clipboardService.clipboardAvailable = false; + return; + } + + this.clipboardService.clipboardAvailable = true; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + try { + const clipboardPermission = await navigator.permissions.query({ name: 'clipboard-read' }); + this.clipboardService.state = clipboardPermission.state; + + // bad support, don't rely on this + clipboardPermission.onchange = function () { + that.clipboardService.state = this.state; + if (this.state === 'denied' || this.state === 'prompt') { + that.clipboardService.clipboardValue = null; + } + }; + } catch (exeption) { + that.clipboardService.state = 'denied'; + that.clipboardService.clipboardValue = null; + console.error(exeption); + } + } +} diff --git a/webapp/packages/core-utils/src/Clipboard/ClipboardService.ts b/webapp/packages/core-utils/src/Clipboard/ClipboardService.ts new file mode 100644 index 0000000000..549a8d3f25 --- /dev/null +++ b/webapp/packages/core-utils/src/Clipboard/ClipboardService.ts @@ -0,0 +1,63 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; + +import { copyToClipboard } from './copyToClipboard'; + +@injectable() +export class ClipboardService { + clipboardAvailable: boolean; + clipboardValue: string | null; + state: PermissionState | null; + + constructor( + private notificationService: NotificationService + ) { + makeObservable(this, { + clipboardValue: observable, + state: observable, + }); + this.clipboardValue = null; + this.state = null; + this.clipboardAvailable = false; + } + + async read(): Promise { + if (!this.clipboardAvailable) { + return null; + } + + try { + const value = await navigator.clipboard.readText(); + this.state = 'granted'; + this.clipboardValue = value; + } catch (exeption) { + console.error(exeption); + this.state = 'denied'; + this.clipboardValue = null; + this.notificationService.logInfo({ title: 'ui_clipboard_access_denied_title', message: 'ui_clipboard_access_denied_message' }); + } + + return this.clipboardValue; + } + + write(value: string, notify: boolean): void { + try { + copyToClipboard(value); + if (notify) { + this.notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Success); + } + } catch (exception) { + this.notificationService.logException(exception, 'ui_copy_to_clipboard_failed_to_copy'); + } + } +} diff --git a/webapp/packages/core-utils/src/copyToClipboard.ts b/webapp/packages/core-utils/src/Clipboard/copyToClipboard.ts similarity index 100% rename from webapp/packages/core-utils/src/copyToClipboard.ts rename to webapp/packages/core-utils/src/Clipboard/copyToClipboard.ts diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index ae34073384..b43ba6adf1 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -10,11 +10,15 @@ export * from './isPropertiesEqual'; export * from './isValuesEqual'; export * from './md5'; export * from './TextTools'; -export * from './copyToClipboard'; +export * from './Clipboard/copyToClipboard'; export * from './declensionOfNumber'; +export * from './replaceMiddle'; export * from './Promises/CancellablePromise'; export * from './Promises/Deferred'; export * from './Promises/deferPromise'; export * from './Promises/PromiseExecutor'; export * from './Promises/PromiseCancelledError'; + +export * from './Clipboard/ClipboardService'; +export * from './Clipboard/ClipboardBootstrap'; diff --git a/webapp/packages/core-utils/src/replaceMiddle.ts b/webapp/packages/core-utils/src/replaceMiddle.ts new file mode 100644 index 0000000000..830ab1586c --- /dev/null +++ b/webapp/packages/core-utils/src/replaceMiddle.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function replaceMiddle(value: string, newMiddle: string, sideLength: number, limiter: number): string { + if (value.length < limiter) { + return value; + } + + return value.substr(0, sideLength) + newMiddle + value.substr(value.length - sideLength); +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts new file mode 100644 index 0000000000..11d0f2de6f --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts @@ -0,0 +1,469 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, ContextMenuService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ComputedContextMenuModel } from '@cloudbeaver/core-dialogs'; +import { ClipboardService, replaceMiddle } from '@cloudbeaver/core-utils'; +import { + getOperationWrappedValue, + IDatabaseDataModel, + operationsWithNullFilter, + ResultSetConstraintAction, + IS_NOT_NULL_ID, + IS_NULL_ID, + ResultSetDataAction, + supportedOperationsFilter, + IDatabaseResultSet, + IDatabaseDataOptions, + ResultSetFormatAction +} from '@cloudbeaver/plugin-data-viewer'; + +import { DataGridContextMenuService, IDataGridCellMenuContext } from '../DataGridContextMenuService'; +import { FilterCustomValueDialog } from './FilterCustomValueDialog'; + +@injectable() +export class DataGridContextMenuFilterService { + private static menuFilterToken = 'menuFilter'; + + constructor( + private dataGridContextMenuService: DataGridContextMenuService, + private contextMenuService: ContextMenuService, + private commonDialogService: CommonDialogService, + private clipboardService: ClipboardService, + ) { + this.dataGridContextMenuService.onRootMenuOpen.addHandler(this.getClipboardValue.bind(this)); + } + + getMenuFilterToken(): string { + return DataGridContextMenuFilterService.menuFilterToken; + } + + private getCellValue(model: IDatabaseDataModel, resultIndex: number, columnIndex: number, rowIndex: number) { + const data = model.source.getAction(resultIndex, ResultSetDataAction); + return data.getCellValue({ column: columnIndex, row: rowIndex }); + } + + private toString(model: IDatabaseDataModel, resultIndex: number, value: any) { + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + return format.toString(value); + } + + private isNull(model: IDatabaseDataModel, resultIndex: number, value: any) { + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + return format.isNull(value); + } + + private getColumnName(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + const data = model.source.getAction(resultIndex, ResultSetDataAction); + return data.getColumn(columnIndex)?.name; + } + + private getColumnSupportedOperations(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + const data = model.source.getAction(resultIndex, ResultSetDataAction); + return data.getColumn(columnIndex)?.supportedOperations.filter(supportedOperationsFilter) || []; + } + + private async applyFilter( + model: IDatabaseDataModel, + resultIndex: number, + columnIndex: number, + operator: string, + filterValue?: any, + ) { + if (model.isLoading() || model.isDisabled(resultIndex)) { + return; + } + + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const columnName = this.getColumnName(model, resultIndex, columnIndex) || ''; + + constraints.setFilter(columnName, operator, filterValue); + + try { + await model.refresh(); + if (model.requestInfo.requestFilter && model.source.options?.whereFilter) { + model.source.options.whereFilter = ''; + } + } catch { + constraints.deleteFilter(columnName); + } + } + + private getFilter(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const columnName = this.getColumnName(model, resultIndex, columnIndex) || ''; + + return constraints.getFilter(columnName); + } + + private getFilters(model: IDatabaseDataModel, resultIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + return constraints.getFilterConstraints(); + } + + private async deleteFilter(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const columnName = this.getColumnName(model, resultIndex, columnIndex) || ''; + + constraints.deleteFilter(columnName); + await model.refresh(); + } + + private async deleteFilters(model: IDatabaseDataModel, resultIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + constraints.deleteFiltersFromConstraints(); + model.source.options.whereFilter = ''; + await model.refresh(); + } + + private async deleteAllConstraints(model: IDatabaseDataModel, resultIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + constraints.deleteAllConstraints(); + model.source.options.whereFilter = ''; + await model.refresh(); + } + + private async getClipboardValue() { + if (this.clipboardService.state && (this.clipboardService.state === 'denied' || this.clipboardService.state === 'prompt')) { + return; + } + + await this.clipboardService.read(); + } + + private async getCustomValue(inputTitle: string, defaultValue: string) { + const customValue = await this.commonDialogService.open(FilterCustomValueDialog, { defaultValue, inputTitle }); + + if (customValue === DialogueStateResult.Rejected || customValue === DialogueStateResult.Resolved) { + return; + } + + return customValue; + } + + register(): void { + this.dataGridContextMenuService.add( + this.contextMenuService.getRootMenuToken(), + { + id: this.getMenuFilterToken(), + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden(context) { + return context.data.model.isDisabled(context.data.resultIndex) + || context.data.model.source.results.length > 1; + }, + order: 1, + title: 'data_grid_table_filter', + icon: '/icons/filter.png', + isPanel: true, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'clipboardValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const supportedOperations = this.getColumnSupportedOperations( + context.data.model, + context.data.resultIndex, + context.data.column + ); + + return !this.clipboardService.clipboardAvailable || supportedOperations.length === 0; + }, + order: 0, + title: 'ui_clipboard', + icon: '/icons/filter_clipboard.png', + panel: new ComputedContextMenuModel({ + id: 'clipboardValuePanel', + menuItemsGetter: context => { + const { model, resultIndex, column } = context.data; + const supportedOperations = this.getColumnSupportedOperations(model, resultIndex, column); + const columnName = this.getColumnName(model, resultIndex, column) || ''; + + if (!this.clipboardService.state || this.clipboardService.state === 'denied' || this.clipboardService.state === 'prompt') { + return [{ + id: 'permission', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled(context) { + return context.data.model.isLoading(); + }, + title: 'data_grid_table_context_menu_filter_clipboard_permission', + icon: '/icons/permissions.png', + onClick: async () => { + await this.clipboardService.read(); + }, + }]; + } + + return supportedOperations + .filter(operation => !operationsWithNullFilter(operation)) + .map(operation => ({ + id: operation.id, + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled(context) { + return context.data.model.isLoading(); + }, + titleGetter: () => { + const clipboardValue = getOperationWrappedValue(this.clipboardService.clipboardValue || '', operation.id); + const clippedClipboardValue = replaceMiddle(clipboardValue, ' ... ', 8, 30); + return `${columnName} ${operation.expression} ${clippedClipboardValue}`; + }, + icon: '/icons/filter_clipboard.png', + onClick: async () => { + await this.applyFilter(model, resultIndex, column, operation.id, this.clipboardService.clipboardValue || ''); + }, + })); + }, + }), + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'cellValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const { model, resultIndex, column, row } = context.data; + const supportedOperations = this.getColumnSupportedOperations( + model, + resultIndex, + column + ); + const cellValue = this.getCellValue(model, resultIndex, column, row); + + return supportedOperations.length === 0 || this.isNull(model, resultIndex, cellValue); + }, + order: 1, + title: 'data_grid_table_filter_cell_value', + icon: '/icons/filter_value.png', + panel: new ComputedContextMenuModel({ + id: 'cellValuePanel', + menuItemsGetter: context => { + const { model, resultIndex, column, row } = context.data; + const supportedOperations = this.getColumnSupportedOperations(model, resultIndex, column); + const cellValue = this.getCellValue(model, resultIndex, column, row); + const columnName = this.getColumnName(model, resultIndex, column) || ''; + + return supportedOperations + .filter(operation => !operationsWithNullFilter(operation)) + .map(operation => { + const stringCellValue = this.toString(model, resultIndex, cellValue); + const wrappedCellValue = getOperationWrappedValue(stringCellValue, operation.id); + const clippedCellValue = replaceMiddle(wrappedCellValue, ' ... ', 8, 30); + const title = `${columnName} ${operation.expression} ${clippedCellValue}`; + + return { + id: operation.id, + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled(context) { + return context.data.model.isLoading(); + }, + title, + icon: '/icons/filter_value.png', + onClick: async () => { + await this.applyFilter(model, resultIndex, column, operation.id, cellValue); + }, + }; + }); + }, + }), + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'customValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const supportedOperations = this.getColumnSupportedOperations( + context.data.model, + context.data.resultIndex, + context.data.column + ); + + return supportedOperations.length === 0; + }, + order: 2, + title: 'data_grid_table_filter_custom_value', + icon: '/icons/filter_custom.png', + separator: true, + panel: new ComputedContextMenuModel({ + id: 'customValuePanel', + menuItemsGetter: context => { + const { model, resultIndex, column, row } = context.data; + const supportedOperations = this.getColumnSupportedOperations(model, resultIndex, column); + const cellValue = this.getCellValue(model, resultIndex, column, row); + const columnName = this.getColumnName(model, resultIndex, column) || ''; + + return supportedOperations + .filter(operation => !operationsWithNullFilter(operation)) + .map(operation => { + const title = `${columnName} ${operation.expression}`; + + return { + id: operation.id, + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isDisabled(context) { + return context.data.model.isLoading(); + }, + title: title + ' ..', + icon: '/icons/filter_custom.png', + onClick: async () => { + const isNull = this.isNull(model, resultIndex, cellValue); + const stringifyCellValue = this.toString(model, resultIndex, cellValue); + const customValue = await this.getCustomValue(title + ':', isNull ? '' : stringifyCellValue); + if (!customValue) { + return; + } + + await this.applyFilter( + model, + resultIndex, + column, + operation.id, + customValue + ); + }, + }; + }); + }, + }), + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'isNullValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const supportedOperations = this.getColumnSupportedOperations( + context.data.model, + context.data.resultIndex, + context.data.column + ); + + return !supportedOperations.some(operation => operation.id === IS_NULL_ID); + }, + order: 3, + titleGetter: context => { + const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column) || ''; + return `${columnName} IS NULL`; + }, + onClick: async context => { + await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NULL_ID); + }, + + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'isNotNullValue', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const supportedOperations = this.getColumnSupportedOperations( + context.data.model, + context.data.resultIndex, + context.data.column + ); + + return !supportedOperations.some(operation => operation.id === IS_NOT_NULL_ID); + }, + order: 4, + separator: true, + titleGetter: context => { + const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column) || ''; + return `${columnName} IS NOT NULL`; + }, + onClick: async context => { + await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NOT_NULL_ID); + }, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'deleteFilter', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const filter = this.getFilter(context.data.model, context.data.resultIndex, context.data.column); + return filter === null; + }, + order: 5, + icon: '/icons/filter_reset.png', + titleGetter: context => { + const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column) || ''; + return `Delete filter for ${columnName}`; + }, + onClick: async context => { + await this.deleteFilter(context.data.model, context.data.resultIndex, context.data.column); + }, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'deleteAllFilters', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { + const filterConstraints = this.getFilters(context.data.model, context.data.resultIndex); + return filterConstraints.length === 0 && !context.data.model.requestInfo.requestFilter; + }, + order: 6, + title: 'data_grid_table_filter_reset_all_filters', + onClick: async context => { + await this.deleteFilters(context.data.model, context.data.resultIndex); + }, + } + ); + this.dataGridContextMenuService.add( + this.getMenuFilterToken(), + { + id: 'deleteFiltersAndSorting', + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + order: 7, + title: 'data_grid_table_delete_filters_and_sorting', + icon: '/icons/erase.png', + onClick: async context => { + await this.deleteAllConstraints(context.data.model, context.data.resultIndex); + }, + } + ); + } +} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx new file mode 100644 index 0000000000..b552746001 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -0,0 +1,100 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react-lite'; +import { useState, useCallback, useRef, useEffect } from 'react'; +import styled, { css } from 'reshadow'; + +import { Button, InputFieldNew } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { ClipboardService } from '@cloudbeaver/core-utils'; + +const styles = css` + CommonDialogWrapper { + min-width: 400px; + } + controls { + display: flex; + flex: 1; + height: 100%; + align-items: center; + margin: auto; + flex-direction: row-reverse; + } + Button { + margin-left: 24px; + } +`; + +interface IPayload { + inputTitle: string; + defaultValue: string | number; +} + +export const FilterCustomValueDialog: DialogComponent = observer( + function FilterCustomValueDialog({ + payload, + resolveDialog, + rejectDialog, + }: DialogComponentProps) { + const clipboardService = useService(ClipboardService); + const inputRef = useRef(null); + + const [value, setValue] = useState(payload.defaultValue); + const handleChange = useCallback((value: string | number) => setValue(value), []); + const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]); + const translate = useTranslate(); + + const getValueFromClipboard = useCallback(async () => { + const value = await clipboardService.read(); + if (value) { + setValue(value); + } + if (inputRef.current) { + inputRef.current.focus(); + } + }, [clipboardService]); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + return styled(styles)( + + + + {clipboardService.clipboardAvailable && ( + + )} + + )} + onReject={rejectDialog} + > + + {payload.inputTitle} + + + ); + } +); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts index ac174fcd04..a0e62b1928 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts @@ -8,6 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IContextMenuItem, IMenuPanel } from '@cloudbeaver/core-dialogs'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; export interface IDataGridCellMenuContext { @@ -19,12 +20,15 @@ export interface IDataGridCellMenuContext { @injectable() export class DataGridContextMenuService { + onRootMenuOpen: IExecutor; static cellContext = 'data-grid-cell-context-menu'; private static menuToken = 'dataGridCell'; constructor( private contextMenuService: ContextMenuService, - ) { } + ) { + this.onRootMenuOpen = new Executor(); + } getMenuToken(): string { return DataGridContextMenuService.menuToken; @@ -43,6 +47,15 @@ export class DataGridContextMenuService { }); } + openMenu( + model: IDatabaseDataModel, + resultIndex: number, + row: number, + column: number + ) { + this.onRootMenuOpen.execute({ model, resultIndex, row, column }); + } + add(panelId: string, menuItem: IContextMenuItem): void { this.contextMenuService.addMenuItem(panelId, menuItem); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts index f916695176..b09fd7f0aa 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts @@ -8,7 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { ContextMenuService } from '@cloudbeaver/core-dialogs'; -import { ESortMode, getNextSortMode, IDatabaseDataModel, ResultSetDataAction, ResultSetSortAction, SortMode } from '@cloudbeaver/plugin-data-viewer'; +import { ESortMode, IDatabaseDataModel, ResultSetConstraintAction, ResultSetDataAction, SortMode } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuService } from './DataGridContextMenuService'; @@ -37,28 +37,28 @@ export class DataGridContextMenuSortingService { sortMode: SortMode ) { const columnName = this.getColumnName(model, resultIndex, columnIndex)!; - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - sorting.setSortMode(columnName, sortMode, true); + constraints.setSortMode(columnName, sortMode, true); await model.refresh(); } private getSortMode(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { const columnName = this.getColumnName(model, resultIndex, columnIndex)!; - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - return sorting.getSortMode(columnName); + return constraints.getSortMode(columnName); } private getSortingConstraints(model: IDatabaseDataModel, resultIndex: number) { - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); - return sorting.getSortingConstraints(); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + return constraints.getSortingConstraints(); } - private async removeSortingConstraints(model: IDatabaseDataModel, resultIndex: number) { - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); + private async removeSortingFromConstraints(model: IDatabaseDataModel, resultIndex: number) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - sorting.removeSortingConstraints(); + constraints.deleteSortingFromConstraints(); await model.refresh(); } @@ -137,13 +137,16 @@ export class DataGridContextMenuSortingService { this.getMenuSortingToken(), { id: 'disableAllSorting', - isPresent: context => { + isPresent(context) { + return context.contextType === DataGridContextMenuService.cellContext; + }, + isHidden: context => { const sortingConstraints = this.getSortingConstraints(context.data.model, context.data.resultIndex); - return context.contextType === DataGridContextMenuService.cellContext && sortingConstraints.length > 1; + return sortingConstraints.length === 0; }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.removeSortingConstraints(context.data.model, context.data.resultIndex); + await this.removeSortingFromConstraints(context.data.model, context.data.resultIndex); }, title: 'data_grid_table_disable_all_sorting', } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx index 7c3b706be9..ec35624e36 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx @@ -51,7 +51,7 @@ export const CellMenu = observer(function TreeNodeMenu({ return styled(cellMenuStyles)( e.stopPropagation()}> - + dataGridContextMenuService.openMenu(model, resultIndex, row, column)}> diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx index a4259cd96f..18427aa144 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx @@ -11,7 +11,7 @@ import styled, { css, use } from 'reshadow'; import { IconOrImage } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { ESortMode, getNextSortMode, IDatabaseDataModel, IDatabaseDataResult, ResultSetSortAction } from '@cloudbeaver/plugin-data-viewer'; +import { ESortMode, getNextSortMode, IDatabaseDataModel, IDatabaseDataResult, ResultSetConstraintAction } from '@cloudbeaver/plugin-data-viewer'; const styles = css` sort-button { @@ -51,8 +51,8 @@ export const SortButton: React.FC = observer(function SortButtton({ className, }) { const translate = useTranslate(); - const sorting = model.source.getAction(resultIndex, ResultSetSortAction); - const currentSortMode = sorting.getSortMode(columnName); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const currentSortMode = constraints.getSortMode(columnName); const loading = model.isLoading(); let icon = '/icons/sort_unknown.png'; @@ -69,7 +69,7 @@ export const SortButton: React.FC = observer(function SortButtton({ } const nextSortMode = getNextSortMode(currentSortMode); - sorting.setSortMode(columnName, nextSortMode, e.ctrlKey || e.metaKey); + constraints.setSortMode(columnName, nextSortMode, e.ctrlKey || e.metaKey); model.refresh(); }; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index 461e1028f8..a6d807092f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -11,6 +11,7 @@ import { ExceptionsCatcherService } from '@cloudbeaver/core-events'; import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer'; +import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { SpreadsheetGrid } from './SpreadsheetGrid'; @@ -21,6 +22,7 @@ export class SpreadsheetBootstrap extends Bootstrap { private dataPresentationService: DataPresentationService, private dataGridSettingsService: DataGridSettingsService, private dataGridContextMenuSortingService: DataGridContextMenuSortingService, + private dataGridContextMenuFilterService: DataGridContextMenuFilterService, exceptionsCatcherService: ExceptionsCatcherService ) { super(); @@ -37,6 +39,7 @@ export class SpreadsheetBootstrap extends Bootstrap { icon: '/icons/grid.png', }); this.dataGridContextMenuSortingService.register(); + this.dataGridContextMenuFilterService.register(); } load(): void | Promise { } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts index 409665d81a..f488705dc6 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/en.ts @@ -1,6 +1,13 @@ export default [ ['data_grid_table_sorting', 'Sorting'], + ['data_grid_table_filter', 'Filters'], + ['data_grid_table_filter_cell_value', 'Cell value'], + ['data_grid_table_filter_custom_value', 'Custom'], + ['data_grid_table_filter_reset_all_filters', 'Reset all filters'], ['data_grid_table_disable_sorting', 'Disabled'], ['data_grid_table_disable_all_sorting', 'Disable all'], + ['data_grid_table_delete_filters_and_sorting', 'Reset filters / sorting'], ['data_grid_table_tooltip_column_header_sort', 'Sort by column'], + ['data_grid_table_context_menu_filter_dialog_title', 'Edit value'], + ['data_grid_table_context_menu_filter_clipboard_permission', 'Give access to clipboard'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts index f3eac693d5..f9272b2294 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/locales/ru.ts @@ -1,6 +1,13 @@ export default [ ['data_grid_table_sorting', 'Сортировка'], + ['data_grid_table_filter', 'Фильтры'], + ['data_grid_table_filter_cell_value', 'Значение ячейки'], + ['data_grid_table_filter_custom_value', 'Произвольный'], + ['data_grid_table_filter_reset_all_filters', 'Сбросить все фильтры'], ['data_grid_table_disable_sorting', 'Отключена'], ['data_grid_table_disable_all_sorting', 'Отменить сортировку всех колонок'], + ['data_grid_table_delete_filters_and_sorting', 'Отменить фильтры / сортировки'], ['data_grid_table_tooltip_column_header_sort', 'Сортировка по колонке'], + ['data_grid_table_context_menu_filter_dialog_title', 'Редактировать значение'], + ['data_grid_table_context_menu_filter_clipboard_permission', 'Дать доступ к буферу обмена'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts index 7d4d6a2f59..8b90e8e597 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts @@ -8,6 +8,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService'; import { DataGridSettingsService } from './DataGridSettingsService'; @@ -22,5 +23,6 @@ export const manifest: PluginManifest = { LocaleService, DataGridContextMenuService, DataGridContextMenuSortingService, + DataGridContextMenuFilterService, ], }; diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 46ca3f080b..f258958b46 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -92,6 +92,7 @@ export class ContainerDataSource extends DatabaseDataSource + extends IDatabaseDataAction { + deleteAllConstraints: () => void; + deleteFilter: (columnName: string) => void; + deleteFiltersFromConstraints: () => void; + deleteSortingFromConstraints: () => void; + setFilter: (columnName: string, operator: string, value?: any) => void; + setSortMode: (columnName: string, sortMode: SortMode, multiple: boolean) => void; + getFilter: (columnName: string) => void; + getSortMode: (columnName: string) => SortMode; + getFilterConstraints: () => SqlDataFilterConstraint[]; + getSortingConstraints: () => SqlDataFilterConstraint[]; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts index 64f08de732..2973233645 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataFormatAction.ts @@ -13,5 +13,6 @@ export interface IDatabaseDataFormatAction { isReadOnly: (key: TKey) => boolean; get: (value: any) => any; + isNull: (value: any) => boolean; toString: (value: any) => string; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts deleted file mode 100644 index 2b5533cc47..0000000000 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataSortAction.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import type { IDatabaseDataAction } from '../IDatabaseDataAction'; -import type { IDatabaseDataResult } from '../IDatabaseDataResult'; -import type { SortMode } from './ResultSet/ResultSetSortAction'; - -export interface IDatabaseDataSortAction - extends IDatabaseDataAction { - setSortMode: (columnName: string, sortMode: SortMode, multiple: boolean) => void; - getSortMode: (columnName: string) => SortMode; -} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts new file mode 100644 index 0000000000..dd6db56ada --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts @@ -0,0 +1,272 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { DataTypeLogicalOperation, ResultDataFormat, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; + +import { DatabaseDataAction } from '../../DatabaseDataAction'; +import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions'; +import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { databaseDataAction } from '../DatabaseDataActionDecorator'; +import { ESortMode, IDatabaseDataConstraintAction, SortMode } from '../IDatabaseDataConstraintAction'; +import type { IResultSetElementKey } from './IResultSetElementKey'; + +export const IS_NULL_ID = 'IS_NULL'; +export const IS_NOT_NULL_ID = 'IS_NOT_NULL'; + +@databaseDataAction() +export class ResultSetConstraintAction extends DatabaseDataAction + implements IDatabaseDataConstraintAction { + static dataFormat = ResultDataFormat.Resultset; + + private deleteConstraint(columnName: string) { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints + .filter(constraint => constraint.attribute !== columnName); + } + + private reorderConstraints() { + if (!this.source.options) { + return; + } + + this.source.options.constraints = this.source.options.constraints + .map((constaint, idx) => ({ ...constaint, orderPosition: idx })); + } + + deleteAllConstraints(): void { + if (!this.source.options) { + return; + } + + this.source.options.constraints = []; + } + + setFilter(columnName: string, operator: string, value?: any): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.source.options.constraints.find(constaint => constaint.attribute === columnName); + + if (currentConstraint) { + currentConstraint.operator = operator; + if (value !== undefined) { + currentConstraint.value = value; + } + if (currentConstraint.value !== undefined && value === undefined) { + delete currentConstraint.value; + } + return; + } + + const constraint: SqlDataFilterConstraint = { + attribute: columnName, + operator, + orderPosition: this.source.options.constraints.length, + }; + + if (value !== undefined) { + constraint.value = value; + } + + this.source.options.constraints.push(constraint); + } + + deleteFilter(columnName: string): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.source.options.constraints.find(constaint => constaint.attribute === columnName); + + if (!currentConstraint) { + return; + } + + if (isConstraintContainsSorting(currentConstraint)) { + deleteFilterFromConstraint(currentConstraint); + } else { + this.deleteConstraint(columnName); + this.reorderConstraints(); + } + } + + getFilter(columnName: string): SqlDataFilterConstraint | null { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.source.options.constraints.find(constaint => constaint.attribute === columnName); + + if (!currentConstraint || (currentConstraint && !isConstraintContainsFilter(currentConstraint))) { + return null; + } + + return currentConstraint; + } + + getFilterConstraints(): SqlDataFilterConstraint[] { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.filter(isConstraintContainsFilter); + } + + deleteFiltersFromConstraints(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + let reorderNeeded = false; + + this.source.options.constraints = this.source.options.constraints.reduce((acc: SqlDataFilterConstraint[], curr) => { + if (isConstraintContainsSorting(curr)) { + deleteFilterFromConstraint(curr); + return [...acc, curr]; + } + reorderNeeded = true; + return acc; + }, []); + + if (reorderNeeded) { + this.reorderConstraints(); + } + } + + deleteSortingFromConstraints(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + let reorderNeeded = false; + + this.source.options.constraints = this.source.options.constraints.reduce((acc: SqlDataFilterConstraint[], curr) => { + if (isConstraintContainsFilter(curr)) { + deleteSortingFromConstraint(curr); + return [...acc, curr]; + } + reorderNeeded = true; + return acc; + }, []); + + if (reorderNeeded) { + this.reorderConstraints(); + } + } + + getSortingConstraints(): SqlDataFilterConstraint[] { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.filter(isConstraintContainsSorting); + } + + setSortMode(columnName: string, sortMode: SortMode, multiple: boolean): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const resetSortMode = sortMode === null; + + if (!multiple) { + this.deleteSortingFromConstraints(); + } + + const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName); + + if (!currentConstraint) { + if (!resetSortMode) { + this.source.options.constraints.push({ + attribute: columnName, + orderPosition: this.source.options.constraints.length, + orderAsc: sortMode === ESortMode.asc, + }); + } + return; + } + + if (!resetSortMode) { + currentConstraint.orderAsc = sortMode === ESortMode.asc; + } else { + if (isConstraintContainsFilter(currentConstraint)) { + deleteSortingFromConstraint(currentConstraint); + } else { + this.deleteConstraint(currentConstraint.attribute); + this.reorderConstraints(); + } + } + } + + getSortMode(columnName: string): SortMode { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName); + + if (!currentConstraint || (currentConstraint && !isConstraintContainsSorting(currentConstraint))) { + return null; + } + + return currentConstraint.orderAsc ? ESortMode.asc : ESortMode.desc; + } +} + +export function supportedOperationsFilter(operation: DataTypeLogicalOperation): boolean { + return operation.argumentCount === 1 || operation.argumentCount === 0; +} + +export function operationsWithNullFilter(operation: DataTypeLogicalOperation): boolean { + return operation.id === IS_NULL_ID || operation.id === IS_NOT_NULL_ID; +} + +export function getNextSortMode(sortMode: SortMode): SortMode { + switch (sortMode) { + case ESortMode.asc: + return ESortMode.desc; + case ESortMode.desc: + return null; + default: + return ESortMode.asc; + } +} + +export function getOperationWrappedValue(value: any, operationId: string): string { + let result = value; + if (operationId === 'LIKE') { + result = `%${result} %`; + } + + result = `'${result}'`; + + return result; +} + +function isConstraintContainsFilter(constraint: SqlDataFilterConstraint) { + return constraint.operator !== undefined; +} + +function isConstraintContainsSorting(constraint: SqlDataFilterConstraint) { + return constraint.orderAsc !== undefined; +} + +function deleteSortingFromConstraint(constraint: SqlDataFilterConstraint) { + delete constraint.orderAsc; + return constraint; +} + +function deleteFilterFromConstraint(constraint: SqlDataFilterConstraint) { + delete constraint.operator; + delete constraint.value; + return constraint; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 927ab760bb..99d7ca5c78 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -1,3 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DatabaseDataAction } from '../../DatabaseDataAction'; @@ -16,7 +24,7 @@ export class ResultSetDataAction extends DatabaseDataAction - implements IDatabaseDataSortAction { - static dataFormat = ResultDataFormat.Resultset; - - private clear() { - if (!this.source.options) { - return; - } - - this.source.options.constraints = []; - } - - private deleteConstraint(columnName: string) { - if (!this.source.options) { - return; - } - - this.source.options.constraints = this.source.options.constraints - .filter(constraint => constraint.attribute !== columnName); - } - - private reorderConstraints() { - if (!this.source.options) { - return; - } - - this.source.options.constraints = this.source.options.constraints - .map((constaint, idx) => ({ ...constaint, orderPosition: idx })); - } - - getSortingConstraints() { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - return this.source.options.constraints - .filter(constraint => constraint.orderAsc !== undefined); - } - - removeSortingConstraints() { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - this.source.options.constraints = this.source.options.constraints - .filter(constaint => constaint.orderAsc === undefined); - } - - setSortMode(columnName: string, sortMode: SortMode, multiple: boolean): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const resetSortMode = sortMode === null; - const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName); - - if (currentConstraint && multiple) { - if (resetSortMode) { - this.deleteConstraint(currentConstraint.attribute); - this.reorderConstraints(); - } else { - currentConstraint.orderAsc = sortMode === ESortMode.asc; - } - return; - } - - if (!multiple) { - this.clear(); - } - - if (!resetSortMode) { - this.source.options.constraints.push({ - attribute: columnName, - orderPosition: this.source.options.constraints.length, - orderAsc: sortMode === ESortMode.asc, - }); - } - } - - getSortMode(columnName: string): SortMode { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName); - - if (!currentConstraint) { - return null; - } - - return currentConstraint.orderAsc ? ESortMode.asc : ESortMode.desc; - } -} - -export function getNextSortMode(sortMode: SortMode): SortMode { - switch (sortMode) { - case ESortMode.asc: - return ESortMode.desc; - case ESortMode.desc: - return null; - default: - return ESortMode.asc; - } -} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 7677a780d6..040338c87a 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -19,7 +19,7 @@ import { DatabaseDataAccessMode, IDatabaseDataSource, IRequestInfo } from './IDa import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext'; export abstract class DatabaseDataSource -implements IDatabaseDataSource { + implements IDatabaseDataSource { access: DatabaseDataAccessMode; dataFormat: ResultDataFormat; supportedDataFormats: ResultDataFormat[]; @@ -73,6 +73,7 @@ implements IDatabaseDataSource { this.requestInfo = { requestDuration: 0, requestMessage: '', + requestFilter: '', source: null, }; this.error = null; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index b037787350..c93d9acd2c 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -17,6 +17,7 @@ import type { IDatabaseExecutionContext } from './IDatabaseExecutionContext'; export interface IRequestInfo { readonly requestDuration: number; readonly requestMessage: string; + readonly requestFilter: string; readonly source: string | null; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx index 93b331934f..b98fc55080 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import styled, { css } from 'reshadow'; import { InlineEditor } from '@cloudbeaver/core-app'; @@ -15,6 +15,7 @@ import { PlaceholderComponent, useObjectRef } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { ResultSetConstraintAction } from '../../DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; import type { ITableHeaderPlaceholderProps } from './TableHeaderService'; const styles = composes( @@ -36,29 +37,46 @@ export const TableWhereFilter: PlaceholderComponent model.source.options?.whereFilter || ''); - const props = useObjectRef({ model, resultIndex, filterValue }); + const hasResult = model.source.hasResult(resultIndex); + let constraints: ResultSetConstraintAction | null = null; + if (hasResult) { + constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + } + const filterConstraints = constraints?.getFilterConstraints(); + let filterValue = model.source.options?.whereFilter || ''; + + if (filterConstraints && filterConstraints.length > 0 && model.source.requestInfo.requestFilter) { + filterValue = model.source.requestInfo.requestFilter; + } + + const setValue = useCallback((filterValue: string) => { + model.source.options.whereFilter = filterValue; + if (constraints && filterConstraints && filterConstraints.length > 0) { + constraints.deleteFiltersFromConstraints(); + } + }, [model.source.options, constraints, filterConstraints]); + + const props = useObjectRef({ model, resultIndex, filterValue, constraints }); const handleApply = useCallback(() => { - const { model, resultIndex, filterValue } = props; + const { model, resultIndex } = props; if (model.isLoading() || model.isDisabled(resultIndex)) { return; } - model.source.options!.whereFilter = filterValue; model.refresh(); }, []); const resetFilter = useCallback(() => { - const { model, resultIndex, filterValue } = props; + const { model, resultIndex, constraints } = props; if (model.isLoading() || model.isDisabled(resultIndex)) { return; } - const applyNeeded = model.source.options?.whereFilter === filterValue; setValue(''); + const applyNeeded = !!model.requestInfo.requestFilter; if (applyNeeded) { - model.source.options!.whereFilter = ''; + constraints?.deleteFiltersFromConstraints(); model.refresh(); } }, []); diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 4e71be961b..3ddf0c8069 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -6,10 +6,11 @@ export * from './DatabaseDataModel/Actions/ResultSet/IResultSetElementKey'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; -export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSortAction'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction'; export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator'; export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction'; +export * from './DatabaseDataModel/Actions/IDatabaseDataConstraintAction'; export * from './DatabaseDataModel/DatabaseDataAction'; export * from './DatabaseDataModel/DatabaseDataActions'; export * from './DatabaseDataModel/DatabaseDataEditor'; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index f4867bc779..6583a56492 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -87,6 +87,7 @@ export class QueryDataSource extends DatabaseDataSource Date: Thu, 27 May 2021 23:41:19 +0300 Subject: [PATCH 2/2] refactor(plugin-data-spreadsheet-new): CB-666 clipboard behaviour --- .../packages/core-app/public/icons/icons.svg | 13 +- .../core-app/public/icons/sort_decrease.png | Bin 3772 -> 0 bytes .../core-app/public/icons/sort_increase.png | Bin 3799 -> 0 bytes .../core-app/public/icons/sort_unknown.png | Bin 3924 -> 0 bytes .../packages/core-bootstrap/src/manifest.ts | 3 +- webapp/packages/core-ui/package.json | 2 + .../src/Clipboard/ClipboardBootstrap.ts | 26 ++ .../src/Clipboard/ClipboardService.ts | 54 ++- webapp/packages/core-ui/src/index.ts | 2 + .../src/Clipboard/ClipboardBootstrap.ts | 50 --- .../src/{Clipboard => }/copyToClipboard.ts | 0 webapp/packages/core-utils/src/index.ts | 5 +- .../DataGridContextMenuFilterService.ts | 328 +++++++----------- .../FilterCustomValueDialog.tsx | 7 +- ....ts => DataGridContextMenuOrderService.ts} | 58 ++-- .../DataGridContextMenuService.ts | 4 +- .../{SortButton.tsx => OrderButton.tsx} | 38 +- .../TableColumnHeader/TableColumnHeader.tsx | 4 +- .../src/SpreadsheetBootstrap.ts | 4 +- .../src/manifest.ts | 4 +- .../Actions/IDatabaseDataAction.ts | 3 +- .../Actions/IDatabaseDataConstraintAction.ts | 28 +- .../ResultSet/ResultSetConstraintAction.ts | 235 +++++++------ .../Actions/ResultSet/ResultSetDataAction.ts | 10 + .../ResultSet/ResultSetFormatAction.ts | 10 +- .../src/DatabaseDataModel/Order.ts | 14 + .../TableHeader/TableWhereFilter.tsx | 30 +- .../packages/plugin-data-viewer/src/index.ts | 1 + 28 files changed, 445 insertions(+), 488 deletions(-) delete mode 100644 webapp/packages/core-app/public/icons/sort_decrease.png delete mode 100644 webapp/packages/core-app/public/icons/sort_increase.png delete mode 100644 webapp/packages/core-app/public/icons/sort_unknown.png create mode 100644 webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts rename webapp/packages/{core-utils => core-ui}/src/Clipboard/ClipboardService.ts (50%) delete mode 100644 webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts rename webapp/packages/core-utils/src/{Clipboard => }/copyToClipboard.ts (100%) rename webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/{DataGridContextMenuSortingService.ts => DataGridContextMenuOrderService.ts} (69%) rename webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/{SortButton.tsx => OrderButton.tsx} (63%) create mode 100644 webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Order.ts diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg index cfa9e11531..884042fb2a 100644 --- a/webapp/packages/core-app/public/icons/icons.svg +++ b/webapp/packages/core-app/public/icons/icons.svg @@ -177,10 +177,13 @@ - - - - + + + + + + + + - diff --git a/webapp/packages/core-app/public/icons/sort_decrease.png b/webapp/packages/core-app/public/icons/sort_decrease.png deleted file mode 100644 index 69e46d483cce1a283dab2524eb48c29500c58f7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3772 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 diff --git a/webapp/packages/core-app/public/icons/sort_increase.png b/webapp/packages/core-app/public/icons/sort_increase.png deleted file mode 100644 index 3bdf742b8764abe560fb72bb0efa9ed5a320c0d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3799 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 F{{T>@w0{5q diff --git a/webapp/packages/core-app/public/icons/sort_unknown.png b/webapp/packages/core-app/public/icons/sort_unknown.png deleted file mode 100644 index 7815511cb7b7f8ce73111408f58cec247dd56690..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3924 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 diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 40c21578e9..36f31b25ae 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -94,8 +94,7 @@ import { RouterService, ScreenService } from '@cloudbeaver/core-routing'; import { EnvironmentService, GraphQLService } from '@cloudbeaver/core-sdk'; import { LocalStorageSaveService, SettingsService } from '@cloudbeaver/core-settings'; import { ThemeService } from '@cloudbeaver/core-theming'; -import { NavigationService, OptionsPanelService } from '@cloudbeaver/core-ui'; -import { ClipboardBootstrap, ClipboardService } from '@cloudbeaver/core-utils'; +import { NavigationService, OptionsPanelService, ClipboardBootstrap, ClipboardService } from '@cloudbeaver/core-ui'; import { ActiveViewService } from '@cloudbeaver/core-view'; export const coreManifest: PluginManifest = { diff --git a/webapp/packages/core-ui/package.json b/webapp/packages/core-ui/package.json index 3298d98f50..c66cdaeec4 100644 --- a/webapp/packages/core-ui/package.json +++ b/webapp/packages/core-ui/package.json @@ -21,6 +21,8 @@ "@cloudbeaver/core-blocks": "^0.1.0", "@cloudbeaver/core-sdk": "^0.1.0", "@cloudbeaver/core-dialogs": "^0.1.0", + "@cloudbeaver/core-events": "^0.1.0", + "@cloudbeaver/core-utils": "^0.1.0", "mobx": "^5.x.x", "mobx-react-lite": "^3.x.x" } diff --git a/webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts b/webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts new file mode 100644 index 0000000000..9886d5e021 --- /dev/null +++ b/webapp/packages/core-ui/src/Clipboard/ClipboardBootstrap.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { ClipboardService } from './ClipboardService'; + +@injectable() +export class ClipboardBootstrap extends Bootstrap { + constructor( + private clipboardService: ClipboardService, + ) { + super(); + } + + register(): void { } + + async load(): Promise { + await this.clipboardService.tryActivateClipboard(); + } +} diff --git a/webapp/packages/core-utils/src/Clipboard/ClipboardService.ts b/webapp/packages/core-ui/src/Clipboard/ClipboardService.ts similarity index 50% rename from webapp/packages/core-utils/src/Clipboard/ClipboardService.ts rename to webapp/packages/core-ui/src/Clipboard/ClipboardService.ts index 549a8d3f25..5402d6027f 100644 --- a/webapp/packages/core-utils/src/Clipboard/ClipboardService.ts +++ b/webapp/packages/core-ui/src/Clipboard/ClipboardService.ts @@ -10,40 +10,48 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { ENotificationType, NotificationService } from '@cloudbeaver/core-events'; - -import { copyToClipboard } from './copyToClipboard'; +import { copyToClipboard } from '@cloudbeaver/core-utils'; @injectable() export class ClipboardService { + private readPermission: PermissionStatus | null; + private permissionUpdate: number; clipboardAvailable: boolean; clipboardValue: string | null; - state: PermissionState | null; + + get state(): PermissionState { + if (this.permissionUpdate > 0 && this.readPermission) { + return this.readPermission.state; + } + + return 'prompt'; + } constructor( private notificationService: NotificationService ) { - makeObservable(this, { + makeObservable(this, { clipboardValue: observable, - state: observable, + permissionUpdate: observable, }); + this.permissionUpdate = 1; + this.readPermission = null; this.clipboardValue = null; - this.state = null; this.clipboardAvailable = false; } async read(): Promise { - if (!this.clipboardAvailable) { + if (!this.clipboardAvailable || this.state === 'denied') { return null; } try { const value = await navigator.clipboard.readText(); - this.state = 'granted'; this.clipboardValue = value; } catch (exeption) { - console.error(exeption); - this.state = 'denied'; + this.clipboardAvailable = false; this.clipboardValue = null; + this.notificationService.logException(exeption, 'Failed to read from clipboard', '', true); this.notificationService.logInfo({ title: 'ui_clipboard_access_denied_title', message: 'ui_clipboard_access_denied_message' }); } @@ -54,10 +62,34 @@ export class ClipboardService { try { copyToClipboard(value); if (notify) { - this.notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Success); + this.notificationService.notify({ title: 'ui_copy_to_clipboard_copied' }, ENotificationType.Info); } } catch (exception) { this.notificationService.logException(exception, 'ui_copy_to_clipboard_failed_to_copy'); } } + + async tryActivateClipboard(): Promise { + if (!navigator.clipboard) { + this.clipboardAvailable = false; + return; + } + + this.clipboardAvailable = true; + await this.updatePermissionStatus(); + if (this.readPermission) { + this.readPermission.addEventListener('change', () => { + this.permissionUpdate++; + }); + } + } + + async updatePermissionStatus(): Promise { + try { + this.readPermission = await navigator.permissions.query({ name: 'clipboard-read' }); + } catch (exeption) { + this.notificationService.logException(exeption, 'Failed to get permission status', '', true); + this.readPermission = null; + } + } } diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index 19bc2275fd..f68342462d 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -2,3 +2,5 @@ export * from './Screens/AppScreen/NavigationService'; export * from './Screens/AppScreen/OptionsPanelService'; export * from './AuthenticationProvider'; export * from './useAuthenticationAction'; +export * from './Clipboard/ClipboardBootstrap'; +export * from './Clipboard/ClipboardService'; diff --git a/webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts b/webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts deleted file mode 100644 index e329b3cdff..0000000000 --- a/webapp/packages/core-utils/src/Clipboard/ClipboardBootstrap.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; - -import { ClipboardService } from './ClipboardService'; - -@injectable() -export class ClipboardBootstrap extends Bootstrap { - constructor( - private clipboardService: ClipboardService, - ) { - super(); - } - - register(): void { } - - async load(): Promise { - if (!navigator.clipboard) { - this.clipboardService.clipboardAvailable = false; - return; - } - - this.clipboardService.clipboardAvailable = true; - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const that = this; - try { - const clipboardPermission = await navigator.permissions.query({ name: 'clipboard-read' }); - this.clipboardService.state = clipboardPermission.state; - - // bad support, don't rely on this - clipboardPermission.onchange = function () { - that.clipboardService.state = this.state; - if (this.state === 'denied' || this.state === 'prompt') { - that.clipboardService.clipboardValue = null; - } - }; - } catch (exeption) { - that.clipboardService.state = 'denied'; - that.clipboardService.clipboardValue = null; - console.error(exeption); - } - } -} diff --git a/webapp/packages/core-utils/src/Clipboard/copyToClipboard.ts b/webapp/packages/core-utils/src/copyToClipboard.ts similarity index 100% rename from webapp/packages/core-utils/src/Clipboard/copyToClipboard.ts rename to webapp/packages/core-utils/src/copyToClipboard.ts diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index b43ba6adf1..0fca1d2e8b 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -10,7 +10,7 @@ export * from './isPropertiesEqual'; export * from './isValuesEqual'; export * from './md5'; export * from './TextTools'; -export * from './Clipboard/copyToClipboard'; +export * from './copyToClipboard'; export * from './declensionOfNumber'; export * from './replaceMiddle'; @@ -19,6 +19,3 @@ export * from './Promises/Deferred'; export * from './Promises/deferPromise'; export * from './Promises/PromiseExecutor'; export * from './Promises/PromiseCancelledError'; - -export * from './Clipboard/ClipboardService'; -export * from './Clipboard/ClipboardBootstrap'; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts index 11d0f2de6f..0570be8f92 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService.ts @@ -7,21 +7,22 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, ContextMenuService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, DialogueStateResult, IContextMenuItem, IMenuContext } from '@cloudbeaver/core-dialogs'; import { ComputedContextMenuModel } from '@cloudbeaver/core-dialogs'; -import { ClipboardService, replaceMiddle } from '@cloudbeaver/core-utils'; +import { ClipboardService } from '@cloudbeaver/core-ui'; +import { replaceMiddle } from '@cloudbeaver/core-utils'; import { - getOperationWrappedValue, + wrapOperationArgument, IDatabaseDataModel, - operationsWithNullFilter, + nullOperationsFilter, ResultSetConstraintAction, IS_NOT_NULL_ID, IS_NULL_ID, ResultSetDataAction, - supportedOperationsFilter, IDatabaseResultSet, IDatabaseDataOptions, - ResultSetFormatAction + ResultSetFormatAction, + isFilterConstraint } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuService, IDataGridCellMenuContext } from '../DataGridContextMenuService'; @@ -33,7 +34,6 @@ export class DataGridContextMenuFilterService { constructor( private dataGridContextMenuService: DataGridContextMenuService, - private contextMenuService: ContextMenuService, private commonDialogService: CommonDialogService, private clipboardService: ClipboardService, ) { @@ -44,31 +44,6 @@ export class DataGridContextMenuFilterService { return DataGridContextMenuFilterService.menuFilterToken; } - private getCellValue(model: IDatabaseDataModel, resultIndex: number, columnIndex: number, rowIndex: number) { - const data = model.source.getAction(resultIndex, ResultSetDataAction); - return data.getCellValue({ column: columnIndex, row: rowIndex }); - } - - private toString(model: IDatabaseDataModel, resultIndex: number, value: any) { - const format = model.source.getAction(resultIndex, ResultSetFormatAction); - return format.toString(value); - } - - private isNull(model: IDatabaseDataModel, resultIndex: number, value: any) { - const format = model.source.getAction(resultIndex, ResultSetFormatAction); - return format.isNull(value); - } - - private getColumnName(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { - const data = model.source.getAction(resultIndex, ResultSetDataAction); - return data.getColumn(columnIndex)?.name; - } - - private getColumnSupportedOperations(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { - const data = model.source.getAction(resultIndex, ResultSetDataAction); - return data.getColumn(columnIndex)?.supportedOperations.filter(supportedOperationsFilter) || []; - } - private async applyFilter( model: IDatabaseDataModel, resultIndex: number, @@ -81,78 +56,56 @@ export class DataGridContextMenuFilterService { } const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const columnName = this.getColumnName(model, resultIndex, columnIndex) || ''; + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(columnIndex)?.label || ''; - constraints.setFilter(columnName, operator, filterValue); - - try { - await model.refresh(); - if (model.requestInfo.requestFilter && model.source.options?.whereFilter) { - model.source.options.whereFilter = ''; - } - } catch { - constraints.deleteFilter(columnName); - } - } - - private getFilter(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const columnName = this.getColumnName(model, resultIndex, columnIndex) || ''; - - return constraints.getFilter(columnName); - } - - private getFilters(model: IDatabaseDataModel, resultIndex: number) { - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - - return constraints.getFilterConstraints(); - } - - private async deleteFilter(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const columnName = this.getColumnName(model, resultIndex, columnIndex) || ''; - - constraints.deleteFilter(columnName); - await model.refresh(); - } - - private async deleteFilters(model: IDatabaseDataModel, resultIndex: number) { - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - - constraints.deleteFiltersFromConstraints(); - model.source.options.whereFilter = ''; - await model.refresh(); - } - - private async deleteAllConstraints(model: IDatabaseDataModel, resultIndex: number) { - const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - - constraints.deleteAllConstraints(); - model.source.options.whereFilter = ''; + constraints.setFilter(columnLabel, operator, filterValue); await model.refresh(); } private async getClipboardValue() { - if (this.clipboardService.state && (this.clipboardService.state === 'denied' || this.clipboardService.state === 'prompt')) { - return; + if (this.clipboardService.state === 'granted') { + await this.clipboardService.read(); } - - await this.clipboardService.read(); } - private async getCustomValue(inputTitle: string, defaultValue: string) { - const customValue = await this.commonDialogService.open(FilterCustomValueDialog, { defaultValue, inputTitle }); + private getGeneralizedMenuItems( + context: IMenuContext, + value: any | (() => any), + icon: string, + ): Array> { + const { model, resultIndex, column } = context.data; + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const supportedOperations = data.getColumnOperations(column); + const columnLabel = data.getColumn(column)?.label || ''; - if (customValue === DialogueStateResult.Rejected || customValue === DialogueStateResult.Resolved) { - return; - } - - return customValue; + return supportedOperations + .filter(operation => !nullOperationsFilter(operation)) + .map(operation => ({ + id: operation.id, + isPresent: () => true, + isDisabled(context) { + return context.data.model.isLoading(); + }, + titleGetter() { + const val = typeof value === 'function' ? value() : value; + const stringifyValue = format.toString(val); + const wrappedValue = wrapOperationArgument(operation.id, stringifyValue); + const clippeddValue = replaceMiddle(wrappedValue, ' ... ', 8, 30); + return `${columnLabel} ${operation.expression} ${clippeddValue}`; + }, + icon, + onClick: async () => { + const val = typeof value === 'function' ? value() : value; + await this.applyFilter(model, resultIndex, column, operation.id, val); + }, + })); } register(): void { this.dataGridContextMenuService.add( - this.contextMenuService.getRootMenuToken(), + this.dataGridContextMenuService.getMenuToken(), { id: this.getMenuFilterToken(), isPresent(context) { @@ -176,13 +129,14 @@ export class DataGridContextMenuFilterService { return context.contextType === DataGridContextMenuService.cellContext; }, isHidden: context => { - const supportedOperations = this.getColumnSupportedOperations( - context.data.model, - context.data.resultIndex, - context.data.column - ); + if (!this.clipboardService.clipboardAvailable || this.clipboardService.state === 'denied') { + return true; + } - return !this.clipboardService.clipboardAvailable || supportedOperations.length === 0; + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); + + return supportedOperations.length === 0; }, order: 0, title: 'ui_clipboard', @@ -190,16 +144,14 @@ export class DataGridContextMenuFilterService { panel: new ComputedContextMenuModel({ id: 'clipboardValuePanel', menuItemsGetter: context => { - const { model, resultIndex, column } = context.data; - const supportedOperations = this.getColumnSupportedOperations(model, resultIndex, column); - const columnName = this.getColumnName(model, resultIndex, column) || ''; + if (context.contextType !== DataGridContextMenuService.cellContext) { + return []; + } - if (!this.clipboardService.state || this.clipboardService.state === 'denied' || this.clipboardService.state === 'prompt') { + if (this.clipboardService.state === 'prompt') { return [{ id: 'permission', - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, + isPresent: () => true, isDisabled(context) { return context.data.model.isLoading(); }, @@ -211,26 +163,9 @@ export class DataGridContextMenuFilterService { }]; } - return supportedOperations - .filter(operation => !operationsWithNullFilter(operation)) - .map(operation => ({ - id: operation.id, - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isDisabled(context) { - return context.data.model.isLoading(); - }, - titleGetter: () => { - const clipboardValue = getOperationWrappedValue(this.clipboardService.clipboardValue || '', operation.id); - const clippedClipboardValue = replaceMiddle(clipboardValue, ' ... ', 8, 30); - return `${columnName} ${operation.expression} ${clippedClipboardValue}`; - }, - icon: '/icons/filter_clipboard.png', - onClick: async () => { - await this.applyFilter(model, resultIndex, column, operation.id, this.clipboardService.clipboardValue || ''); - }, - })); + const valueGetter = () => this.clipboardService.clipboardValue || ''; + const items = this.getGeneralizedMenuItems(context, valueGetter, '/icons/filter_clipboard.png'); + return items; }, }), } @@ -244,14 +179,11 @@ export class DataGridContextMenuFilterService { }, isHidden: context => { const { model, resultIndex, column, row } = context.data; - const supportedOperations = this.getColumnSupportedOperations( - model, - resultIndex, - column - ); - const cellValue = this.getCellValue(model, resultIndex, column, row); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const supportedOperations = data.getColumnOperations(column); - return supportedOperations.length === 0 || this.isNull(model, resultIndex, cellValue); + return supportedOperations.length === 0 || format.isNull(data.getCellValue({ column, row })); }, order: 1, title: 'data_grid_table_filter_cell_value', @@ -260,33 +192,10 @@ export class DataGridContextMenuFilterService { id: 'cellValuePanel', menuItemsGetter: context => { const { model, resultIndex, column, row } = context.data; - const supportedOperations = this.getColumnSupportedOperations(model, resultIndex, column); - const cellValue = this.getCellValue(model, resultIndex, column, row); - const columnName = this.getColumnName(model, resultIndex, column) || ''; - - return supportedOperations - .filter(operation => !operationsWithNullFilter(operation)) - .map(operation => { - const stringCellValue = this.toString(model, resultIndex, cellValue); - const wrappedCellValue = getOperationWrappedValue(stringCellValue, operation.id); - const clippedCellValue = replaceMiddle(wrappedCellValue, ' ... ', 8, 30); - const title = `${columnName} ${operation.expression} ${clippedCellValue}`; - - return { - id: operation.id, - isPresent(context) { - return context.contextType === DataGridContextMenuService.cellContext; - }, - isDisabled(context) { - return context.data.model.isLoading(); - }, - title, - icon: '/icons/filter_value.png', - onClick: async () => { - await this.applyFilter(model, resultIndex, column, operation.id, cellValue); - }, - }; - }); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const cellValue = data.getCellValue({ column, row }); + const items = this.getGeneralizedMenuItems(context, cellValue, '/icons/filter_value.png'); + return items; }, }), } @@ -299,11 +208,8 @@ export class DataGridContextMenuFilterService { return context.contextType === DataGridContextMenuService.cellContext; }, isHidden: context => { - const supportedOperations = this.getColumnSupportedOperations( - context.data.model, - context.data.resultIndex, - context.data.column - ); + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); return supportedOperations.length === 0; }, @@ -315,14 +221,16 @@ export class DataGridContextMenuFilterService { id: 'customValuePanel', menuItemsGetter: context => { const { model, resultIndex, column, row } = context.data; - const supportedOperations = this.getColumnSupportedOperations(model, resultIndex, column); - const cellValue = this.getCellValue(model, resultIndex, column, row); - const columnName = this.getColumnName(model, resultIndex, column) || ''; + const format = model.source.getAction(resultIndex, ResultSetFormatAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(column); + const cellValue = data.getCellValue({ column, row }); + const columnLabel = data.getColumn(column)?.label || ''; return supportedOperations - .filter(operation => !operationsWithNullFilter(operation)) + .filter(operation => !nullOperationsFilter(operation)) .map(operation => { - const title = `${columnName} ${operation.expression}`; + const title = `${columnLabel} ${operation.expression}`; return { id: operation.id, @@ -335,10 +243,17 @@ export class DataGridContextMenuFilterService { title: title + ' ..', icon: '/icons/filter_custom.png', onClick: async () => { - const isNull = this.isNull(model, resultIndex, cellValue); - const stringifyCellValue = this.toString(model, resultIndex, cellValue); - const customValue = await this.getCustomValue(title + ':', isNull ? '' : stringifyCellValue); - if (!customValue) { + const isNull = format.isNull(cellValue); + const stringifyCellValue = format.toString(cellValue); + const customValue = await this.commonDialogService.open( + FilterCustomValueDialog, + { + defaultValue: isNull ? '' : stringifyCellValue, + inputTitle: title + ':', + } + ); + + if (customValue === DialogueStateResult.Rejected || customValue === DialogueStateResult.Resolved) { return; } @@ -364,18 +279,17 @@ export class DataGridContextMenuFilterService { return context.contextType === DataGridContextMenuService.cellContext; }, isHidden: context => { - const supportedOperations = this.getColumnSupportedOperations( - context.data.model, - context.data.resultIndex, - context.data.column - ); + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); return !supportedOperations.some(operation => operation.id === IS_NULL_ID); }, order: 3, + icon: '/icons/filter_value.png', titleGetter: context => { - const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column) || ''; - return `${columnName} IS NULL`; + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(context.data.column)?.label || ''; + return `${columnLabel} IS NULL`; }, onClick: async context => { await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NULL_ID); @@ -391,19 +305,18 @@ export class DataGridContextMenuFilterService { return context.contextType === DataGridContextMenuService.cellContext; }, isHidden: context => { - const supportedOperations = this.getColumnSupportedOperations( - context.data.model, - context.data.resultIndex, - context.data.column - ); + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const supportedOperations = data.getColumnOperations(context.data.column); return !supportedOperations.some(operation => operation.id === IS_NOT_NULL_ID); }, order: 4, + icon: '/icons/filter_value.png', separator: true, titleGetter: context => { - const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column) || ''; - return `${columnName} IS NOT NULL`; + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(context.data.column)?.label || ''; + return `${columnLabel} IS NOT NULL`; }, onClick: async context => { await this.applyFilter(context.data.model, context.data.resultIndex, context.data.column, IS_NOT_NULL_ID); @@ -418,17 +331,30 @@ export class DataGridContextMenuFilterService { return context.contextType === DataGridContextMenuService.cellContext; }, isHidden: context => { - const filter = this.getFilter(context.data.model, context.data.resultIndex, context.data.column); - return filter === null; + const { model, resultIndex, column } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + + const columnLabel = data.getColumn(column)?.label || ''; + const currentConstraint = constraints.get(columnLabel); + + return !currentConstraint || !isFilterConstraint(currentConstraint); }, order: 5, icon: '/icons/filter_reset.png', titleGetter: context => { - const columnName = this.getColumnName(context.data.model, context.data.resultIndex, context.data.column) || ''; - return `Delete filter for ${columnName}`; + const data = context.data.model.source.getAction(context.data.resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(context.data.column)?.name || ''; + return `Delete filter for ${columnLabel}`; }, onClick: async context => { - await this.deleteFilter(context.data.model, context.data.resultIndex, context.data.column); + const { model, resultIndex, column } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const data = model.source.getAction(resultIndex, ResultSetDataAction); + const columnLabel = data.getColumn(column)?.label || ''; + + constraints.deleteFilter(columnLabel); + await model.refresh(); }, } ); @@ -440,13 +366,20 @@ export class DataGridContextMenuFilterService { return context.contextType === DataGridContextMenuService.cellContext; }, isHidden: context => { - const filterConstraints = this.getFilters(context.data.model, context.data.resultIndex); - return filterConstraints.length === 0 && !context.data.model.requestInfo.requestFilter; + const { model, resultIndex } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + const filterConstraints = constraints.getFilterConstraints(); + + return filterConstraints.length === 0 && !model.requestInfo.requestFilter; }, order: 6, title: 'data_grid_table_filter_reset_all_filters', onClick: async context => { - await this.deleteFilters(context.data.model, context.data.resultIndex); + const { model, resultIndex } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + constraints.deleteDataFilters(); + await model.refresh(); }, } ); @@ -461,7 +394,12 @@ export class DataGridContextMenuFilterService { title: 'data_grid_table_delete_filters_and_sorting', icon: '/icons/erase.png', onClick: async context => { - await this.deleteAllConstraints(context.data.model, context.data.resultIndex); + const { model, resultIndex } = context.data; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + + constraints.deleteAll(); + model.source.options.whereFilter = ''; + await model.refresh(); }, } ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx index b552746001..cc00caf188 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -14,7 +14,7 @@ import { Button, InputFieldNew } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { ClipboardService } from '@cloudbeaver/core-utils'; +import { ClipboardService } from '@cloudbeaver/core-ui'; const styles = css` CommonDialogWrapper { @@ -48,7 +48,6 @@ export const FilterCustomValueDialog: DialogComponent const inputRef = useRef(null); const [value, setValue] = useState(payload.defaultValue); - const handleChange = useCallback((value: string | number) => setValue(value), []); const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]); const translate = useTranslate(); @@ -77,7 +76,7 @@ export const FilterCustomValueDialog: DialogComponent - {clipboardService.clipboardAvailable && ( + {clipboardService.clipboardAvailable && clipboardService.state !== 'denied' && ( @@ -90,7 +89,7 @@ export const FilterCustomValueDialog: DialogComponent ref={inputRef} name='customValue' value={value} - onChange={handleChange} + onChange={setValue} > {payload.inputTitle} diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts similarity index 69% rename from webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts rename to webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts index b09fd7f0aa..6299c130d4 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuSortingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuOrderService.ts @@ -7,22 +7,20 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService } from '@cloudbeaver/core-dialogs'; -import { ESortMode, IDatabaseDataModel, ResultSetConstraintAction, ResultSetDataAction, SortMode } from '@cloudbeaver/plugin-data-viewer'; +import { EOrder, IDatabaseDataModel, ResultSetConstraintAction, ResultSetDataAction, Order } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuService } from './DataGridContextMenuService'; @injectable() -export class DataGridContextMenuSortingService { - private static menuSortingToken = 'menuSorting'; +export class DataGridContextMenuOrderService { + private static menuOrderToken = 'menuOrder'; constructor( private dataGridContextMenuService: DataGridContextMenuService, - private contextMenuService: ContextMenuService, ) { } - getMenuSortingToken(): string { - return DataGridContextMenuSortingService.menuSortingToken; + getMenuOrderToken(): string { + return DataGridContextMenuOrderService.menuOrderToken; } private getColumnName(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { @@ -30,43 +28,43 @@ export class DataGridContextMenuSortingService { return data.getColumn(columnIndex)?.name; } - private async changeSortMode( + private async changeOrder( model: IDatabaseDataModel, resultIndex: number, columnIndex: number, - sortMode: SortMode + order: Order ) { const columnName = this.getColumnName(model, resultIndex, columnIndex)!; const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - constraints.setSortMode(columnName, sortMode, true); + constraints.setOrder(columnName, order, true); await model.refresh(); } - private getSortMode(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { + private getOrder(model: IDatabaseDataModel, resultIndex: number, columnIndex: number) { const columnName = this.getColumnName(model, resultIndex, columnIndex)!; const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - return constraints.getSortMode(columnName); + return constraints.getOrder(columnName); } private getSortingConstraints(model: IDatabaseDataModel, resultIndex: number) { const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - return constraints.getSortingConstraints(); + return constraints.getOrderConstraints(); } private async removeSortingFromConstraints(model: IDatabaseDataModel, resultIndex: number) { const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - constraints.deleteSortingFromConstraints(); + constraints.deleteOrders(); await model.refresh(); } register(): void { this.dataGridContextMenuService.add( - this.contextMenuService.getRootMenuToken(), + this.dataGridContextMenuService.getMenuToken(), { - id: this.getMenuSortingToken(), + id: this.getMenuOrderToken(), isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, @@ -77,7 +75,7 @@ export class DataGridContextMenuSortingService { } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { id: 'asc', isPresent(context) { @@ -85,18 +83,18 @@ export class DataGridContextMenuSortingService { }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.asc); + await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, EOrder.asc); }, type: 'radio', - isChecked: context => this.getSortMode( + isChecked: context => this.getOrder( context.data.model, context.data.resultIndex, - context.data.column) === ESortMode.asc, + context.data.column) === EOrder.asc, title: 'ASC', } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { id: 'desc', isPresent(context) { @@ -104,29 +102,29 @@ export class DataGridContextMenuSortingService { }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, ESortMode.desc); + await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, EOrder.desc); }, type: 'radio', - isChecked: context => this.getSortMode( + isChecked: context => this.getOrder( context.data.model, context.data.resultIndex, - context.data.column) === ESortMode.desc, + context.data.column) === EOrder.desc, title: 'DESC', } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { - id: 'disableSorting', + id: 'disableOrder', isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, isDisabled: context => context.data.model.isLoading(), onClick: async context => { - await this.changeSortMode(context.data.model, context.data.resultIndex, context.data.column, null); + await this.changeOrder(context.data.model, context.data.resultIndex, context.data.column, null); }, type: 'radio', - isChecked: context => this.getSortMode( + isChecked: context => this.getOrder( context.data.model, context.data.resultIndex, context.data.column) === null, @@ -134,9 +132,9 @@ export class DataGridContextMenuSortingService { } ); this.dataGridContextMenuService.add( - this.getMenuSortingToken(), + this.getMenuOrderToken(), { - id: 'disableAllSorting', + id: 'disableOrders', isPresent(context) { return context.contextType === DataGridContextMenuService.cellContext; }, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts index a0e62b1928..4991f94225 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuService.ts @@ -44,7 +44,7 @@ export class DataGridContextMenuService { menuId: this.getMenuToken(), contextType: DataGridContextMenuService.cellContext, data: { model, resultIndex, row, column }, - }); + }, this.getMenuToken()); } openMenu( @@ -52,7 +52,7 @@ export class DataGridContextMenuService { resultIndex: number, row: number, column: number - ) { + ): void { this.onRootMenuOpen.execute({ model, resultIndex, row, column }); } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx similarity index 63% rename from webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx rename to webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx index 18427aa144..3d0c5e8da9 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/SortButton.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/OrderButton.tsx @@ -11,10 +11,10 @@ import styled, { css, use } from 'reshadow'; import { IconOrImage } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { ESortMode, getNextSortMode, IDatabaseDataModel, IDatabaseDataResult, ResultSetConstraintAction } from '@cloudbeaver/plugin-data-viewer'; +import { EOrder, getNextOrder, IDatabaseDataModel, IDatabaseDataResult, ResultSetConstraintAction } from '@cloudbeaver/plugin-data-viewer'; const styles = css` - sort-button { + order-button { margin-left: 4px; display: flex; padding: 2px 4px; @@ -25,13 +25,13 @@ const styles = css` box-sizing: border-box; cursor: pointer; } - sort-button > IconOrImage { - width: 8px; + order-button > IconOrImage { + width: 12px; } - sort-button:hover > IconOrImage { - width: 9px; + order-button:hover > IconOrImage { + width: 13px; } - sort-button[|disabled] { + order-button[|disabled] { opacity: 0.7; cursor: default; } @@ -44,7 +44,7 @@ interface Props { className?: string; } -export const SortButton: React.FC = observer(function SortButtton({ +export const OrderButton: React.FC = observer(function OrderButtton({ model, resultIndex, columnName, @@ -52,14 +52,14 @@ export const SortButton: React.FC = observer(function SortButtton({ }) { const translate = useTranslate(); const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const currentSortMode = constraints.getSortMode(columnName); + const currentOrder = constraints.getOrder(columnName); const loading = model.isLoading(); - let icon = '/icons/sort_unknown.png'; - if (currentSortMode === ESortMode.asc) { - icon = '/icons/sort_increase.png'; - } else if (currentSortMode === ESortMode.desc) { - icon = '/icons/sort_decrease.png'; + let icon = 'sort-arrow-unknown'; + if (currentOrder === EOrder.asc) { + icon = 'sort-arrow-up'; + } else if (currentOrder === EOrder.desc) { + icon = 'sort-arrow-down'; } const handleSort = (e: React.MouseEvent) => { @@ -68,20 +68,20 @@ export const SortButton: React.FC = observer(function SortButtton({ return; } - const nextSortMode = getNextSortMode(currentSortMode); - constraints.setSortMode(columnName, nextSortMode, e.ctrlKey || e.metaKey); + const nextOrder = getNextOrder(currentOrder); + constraints.setOrder(columnName, nextOrder, e.ctrlKey || e.metaKey); model.refresh(); }; return styled(styles)( - - - + + ); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index ccf1d222bc..2756b332e7 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -16,7 +16,7 @@ import { ResultSetDataAction } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext'; import { DataGridSelectionContext } from '../DataGridSelection/DataGridSelectionContext'; -import { SortButton } from './SortButton'; +import { OrderButton } from './OrderButton'; const headerStyles = css` table-header { @@ -78,7 +78,7 @@ export const TableColumnHeader: React.FC> = observer(fu {columnName} {sortable && column?.name && ( - + )} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index a6d807092f..279ad2cda0 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -12,7 +12,7 @@ import { ResultDataFormat } from '@cloudbeaver/core-sdk'; import { DataPresentationService } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; -import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService'; +import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { SpreadsheetGrid } from './SpreadsheetGrid'; @@ -21,7 +21,7 @@ export class SpreadsheetBootstrap extends Bootstrap { constructor( private dataPresentationService: DataPresentationService, private dataGridSettingsService: DataGridSettingsService, - private dataGridContextMenuSortingService: DataGridContextMenuSortingService, + private dataGridContextMenuSortingService: DataGridContextMenuOrderService, private dataGridContextMenuFilterService: DataGridContextMenuFilterService, exceptionsCatcherService: ExceptionsCatcherService ) { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts index 8b90e8e597..7f608fd7ed 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/manifest.ts @@ -9,8 +9,8 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { DataGridContextMenuFilterService } from './DataGrid/DataGridContextMenu/DataGridContextMenuFilter/DataGridContextMenuFilterService'; +import { DataGridContextMenuOrderService } from './DataGrid/DataGridContextMenu/DataGridContextMenuOrderService'; import { DataGridContextMenuService } from './DataGrid/DataGridContextMenu/DataGridContextMenuService'; -import { DataGridContextMenuSortingService } from './DataGrid/DataGridContextMenu/DataGridContextMenuSortingService'; import { DataGridSettingsService } from './DataGridSettingsService'; import { LocaleService } from './LocaleService'; import { SpreadsheetBootstrap } from './SpreadsheetBootstrap'; @@ -22,7 +22,7 @@ export const manifest: PluginManifest = { DataGridSettingsService, LocaleService, DataGridContextMenuService, - DataGridContextMenuSortingService, + DataGridContextMenuOrderService, DataGridContextMenuFilterService, ], }; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts index c58bcdf289..9dffb9d659 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { SqlResultColumn } from '@cloudbeaver/core-sdk'; +import type { DataTypeLogicalOperation, SqlResultColumn } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataAction } from '../IDatabaseDataAction'; import type { IDatabaseDataResult } from '../IDatabaseDataResult'; @@ -16,4 +16,5 @@ export interface IDatabaseResultDataAction { getCellValue: (cell: IResultSetElementKey) => any; getColumn: (columnIndex: number) => SqlResultColumn | undefined; + getColumnOperations: (columnIndex: number) => DataTypeLogicalOperation[]; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts index 10b991e94c..14377ba5ea 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataConstraintAction.ts @@ -10,24 +10,20 @@ import type { SqlDataFilterConstraint } from '@cloudbeaver/core-sdk'; import type { IDatabaseDataAction } from '../IDatabaseDataAction'; import type { IDatabaseDataResult } from '../IDatabaseDataResult'; - -export enum ESortMode { - 'asc' = 'asc', - 'desc' = 'desc' -} - -export type SortMode = ESortMode | null; +import type { Order } from '../Order'; export interface IDatabaseDataConstraintAction extends IDatabaseDataAction { - deleteAllConstraints: () => void; - deleteFilter: (columnName: string) => void; - deleteFiltersFromConstraints: () => void; - deleteSortingFromConstraints: () => void; - setFilter: (columnName: string, operator: string, value?: any) => void; - setSortMode: (columnName: string, sortMode: SortMode, multiple: boolean) => void; - getFilter: (columnName: string) => void; - getSortMode: (columnName: string) => SortMode; + setFilter: (attribute: string, operator: string, value?: any) => void; + setOrder: (attribute: string, order: Order, multiple: boolean) => void; + deleteAll: () => void; + deleteFilter: (attribute: string) => void; + deleteFilters: () => void; + deleteOrder: (attribute: string) => void; + deleteOrders: () => void; + deleteDataFilters: () => void; + get: (attribute: string) => SqlDataFilterConstraint | undefined; + getOrder: (attribute: string) => Order; getFilterConstraints: () => SqlDataFilterConstraint[]; - getSortingConstraints: () => SqlDataFilterConstraint[]; + getOrderConstraints: () => SqlDataFilterConstraint[]; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts index dd6db56ada..2ad0828a5f 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetConstraintAction.ts @@ -11,8 +11,9 @@ import { DataTypeLogicalOperation, ResultDataFormat, SqlDataFilterConstraint } f import { DatabaseDataAction } from '../../DatabaseDataAction'; import type { IDatabaseDataOptions } from '../../IDatabaseDataOptions'; import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { EOrder, Order } from '../../Order'; import { databaseDataAction } from '../DatabaseDataActionDecorator'; -import { ESortMode, IDatabaseDataConstraintAction, SortMode } from '../IDatabaseDataConstraintAction'; +import type { IDatabaseDataConstraintAction } from '../IDatabaseDataConstraintAction'; import type { IResultSetElementKey } from './IResultSetElementKey'; export const IS_NULL_ID = 'IS_NULL'; @@ -23,13 +24,13 @@ export class ResultSetConstraintAction extends DatabaseDataAction { static dataFormat = ResultDataFormat.Resultset; - private deleteConstraint(columnName: string) { + private deleteConstraint(attribute: string) { if (!this.source.options) { return; } this.source.options.constraints = this.source.options.constraints - .filter(constraint => constraint.attribute !== columnName); + .filter(constraint => constraint.attribute !== attribute); } private reorderConstraints() { @@ -41,7 +42,24 @@ export class ResultSetConstraintAction extends DatabaseDataAction ({ ...constaint, orderPosition: idx })); } - deleteAllConstraints(): void { + private deleteEmptyConstraint(attribute: string) { + const constraint = this.get(attribute); + + if (constraint && !isFilterConstraint(constraint) && !isOrderConstraint(constraint)) { + this.deleteConstraint(attribute); + this.reorderConstraints(); + } + } + + get(attribute: string): SqlDataFilterConstraint | undefined { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + return this.source.options.constraints.find(constaint => constaint.attribute === attribute); + } + + deleteAll(): void { if (!this.source.options) { return; } @@ -49,26 +67,84 @@ export class ResultSetConstraintAction extends DatabaseDataAction constaint.attribute === columnName); + const newConstraints: SqlDataFilterConstraint[] = []; + + for (const constraint of this.source.options.constraints) { + deleteLogicalOperationFromConstraint(constraint); + if (isOrderConstraint(constraint)) { + newConstraints.push({ ...constraint, orderPosition: newConstraints.length }); + } + } + + this.source.options.constraints = newConstraints; + } + + deleteOrders(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const newConstraints: SqlDataFilterConstraint[] = []; + + for (const constraint of this.source.options.constraints) { + deleteOrderFromConstraint(constraint); + if (isFilterConstraint(constraint)) { + newConstraints.push({ ...constraint, orderPosition: newConstraints.length }); + } + } + + this.source.options.constraints = newConstraints; + } + + deleteOrder(attribute: string): void { + const constraint = this.get(attribute); + if (constraint) { + deleteOrderFromConstraint(constraint); + this.deleteEmptyConstraint(attribute); + } + } + + deleteDataFilters(): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + this.deleteFilters(); + this.source.options.whereFilter = ''; + } + + setFilter(attribute: string, operator: string, value?: any): void { + if (!this.source.options) { + throw new Error('Options must be provided'); + } + + const currentConstraint = this.get(attribute); if (currentConstraint) { currentConstraint.operator = operator; if (value !== undefined) { currentConstraint.value = value; - } - if (currentConstraint.value !== undefined && value === undefined) { + } else if (currentConstraint.value !== undefined) { delete currentConstraint.value; } return; } const constraint: SqlDataFilterConstraint = { - attribute: columnName, + attribute, operator, orderPosition: this.source.options.constraints.length, }; @@ -80,126 +156,51 @@ export class ResultSetConstraintAction extends DatabaseDataAction constaint.attribute === columnName); - - if (!currentConstraint) { - return; - } - - if (isConstraintContainsSorting(currentConstraint)) { - deleteFilterFromConstraint(currentConstraint); - } else { - this.deleteConstraint(columnName); - this.reorderConstraints(); - } - } - - getFilter(columnName: string): SqlDataFilterConstraint | null { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const currentConstraint = this.source.options.constraints.find(constaint => constaint.attribute === columnName); - - if (!currentConstraint || (currentConstraint && !isConstraintContainsFilter(currentConstraint))) { - return null; - } - - return currentConstraint; - } - getFilterConstraints(): SqlDataFilterConstraint[] { if (!this.source.options) { throw new Error('Options must be provided'); } - return this.source.options.constraints.filter(isConstraintContainsFilter); + return this.source.options.constraints.filter(isFilterConstraint); } - deleteFiltersFromConstraints(): void { + getOrderConstraints(): SqlDataFilterConstraint[] { if (!this.source.options) { throw new Error('Options must be provided'); } - let reorderNeeded = false; - - this.source.options.constraints = this.source.options.constraints.reduce((acc: SqlDataFilterConstraint[], curr) => { - if (isConstraintContainsSorting(curr)) { - deleteFilterFromConstraint(curr); - return [...acc, curr]; - } - reorderNeeded = true; - return acc; - }, []); - - if (reorderNeeded) { - this.reorderConstraints(); - } + return this.source.options.constraints.filter(isOrderConstraint); } - deleteSortingFromConstraints(): void { + setOrder(attribute: string, order: Order, multiple: boolean): void { if (!this.source.options) { throw new Error('Options must be provided'); } - let reorderNeeded = false; - - this.source.options.constraints = this.source.options.constraints.reduce((acc: SqlDataFilterConstraint[], curr) => { - if (isConstraintContainsFilter(curr)) { - deleteSortingFromConstraint(curr); - return [...acc, curr]; - } - reorderNeeded = true; - return acc; - }, []); - - if (reorderNeeded) { - this.reorderConstraints(); - } - } - - getSortingConstraints(): SqlDataFilterConstraint[] { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - return this.source.options.constraints.filter(isConstraintContainsSorting); - } - - setSortMode(columnName: string, sortMode: SortMode, multiple: boolean): void { - if (!this.source.options) { - throw new Error('Options must be provided'); - } - - const resetSortMode = sortMode === null; + const resetOrder = order === null; if (!multiple) { - this.deleteSortingFromConstraints(); + this.deleteOrders(); } - const currentConstraint = this.source.options.constraints.find(constraint => constraint.attribute === columnName); + const currentConstraint = this.get(attribute); if (!currentConstraint) { - if (!resetSortMode) { + if (!resetOrder) { this.source.options.constraints.push({ - attribute: columnName, + attribute, orderPosition: this.source.options.constraints.length, - orderAsc: sortMode === ESortMode.asc, + orderAsc: order === EOrder.asc, }); } return; } - if (!resetSortMode) { - currentConstraint.orderAsc = sortMode === ESortMode.asc; + if (!resetOrder) { + currentConstraint.orderAsc = order === EOrder.asc; } else { - if (isConstraintContainsFilter(currentConstraint)) { - deleteSortingFromConstraint(currentConstraint); + if (isFilterConstraint(currentConstraint)) { + deleteOrderFromConstraint(currentConstraint); } else { this.deleteConstraint(currentConstraint.attribute); this.reorderConstraints(); @@ -207,42 +208,38 @@ export class ResultSetConstraintAction extends DatabaseDataAction constraint.attribute === columnName); + const currentConstraint = this.get(attribute); - if (!currentConstraint || (currentConstraint && !isConstraintContainsSorting(currentConstraint))) { + if (!currentConstraint || !isOrderConstraint(currentConstraint)) { return null; } - return currentConstraint.orderAsc ? ESortMode.asc : ESortMode.desc; + return currentConstraint.orderAsc ? EOrder.asc : EOrder.desc; } } -export function supportedOperationsFilter(operation: DataTypeLogicalOperation): boolean { - return operation.argumentCount === 1 || operation.argumentCount === 0; -} - -export function operationsWithNullFilter(operation: DataTypeLogicalOperation): boolean { +export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean { return operation.id === IS_NULL_ID || operation.id === IS_NOT_NULL_ID; } -export function getNextSortMode(sortMode: SortMode): SortMode { - switch (sortMode) { - case ESortMode.asc: - return ESortMode.desc; - case ESortMode.desc: +export function getNextOrder(order: Order): Order { + switch (order) { + case EOrder.asc: + return EOrder.desc; + case EOrder.desc: return null; default: - return ESortMode.asc; + return EOrder.asc; } } -export function getOperationWrappedValue(value: any, operationId: string): string { - let result = value; +export function wrapOperationArgument(operationId: string, argument: any): string { + let result = argument; if (operationId === 'LIKE') { result = `%${result} %`; } @@ -252,20 +249,20 @@ export function getOperationWrappedValue(value: any, operationId: string): strin return result; } -function isConstraintContainsFilter(constraint: SqlDataFilterConstraint) { +export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean { return constraint.operator !== undefined; } -function isConstraintContainsSorting(constraint: SqlDataFilterConstraint) { +function isOrderConstraint(constraint: SqlDataFilterConstraint) { return constraint.orderAsc !== undefined; } -function deleteSortingFromConstraint(constraint: SqlDataFilterConstraint) { +function deleteOrderFromConstraint(constraint: SqlDataFilterConstraint) { delete constraint.orderAsc; return constraint; } -function deleteFilterFromConstraint(constraint: SqlDataFilterConstraint) { +function deleteLogicalOperationFromConstraint(constraint: SqlDataFilterConstraint) { delete constraint.operator; delete constraint.value; return constraint; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts index 99d7ca5c78..22c57d2651 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -34,4 +34,14 @@ export class ResultSetDataAction extends DatabaseDataAction operation.argumentCount === 1 || operation.argumentCount === 0); + } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts index 332263c31b..bcfb4a50db 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction.ts @@ -40,13 +40,7 @@ export class ResultSetFormatAction extends DatabaseDataAction 0 && model.source.requestInfo.requestFilter) { - filterValue = model.source.requestInfo.requestFilter; + if (hasResult) { + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); + if (constraints.getFilterConstraints().length > 0 && model.source.requestInfo.requestFilter) { + filterValue = model.source.requestInfo.requestFilter; + } } const setValue = useCallback((filterValue: string) => { - model.source.options.whereFilter = filterValue; - if (constraints && filterConstraints && filterConstraints.length > 0) { - constraints.deleteFiltersFromConstraints(); - } - }, [model.source.options, constraints, filterConstraints]); + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); - const props = useObjectRef({ model, resultIndex, filterValue, constraints }); + model.source.options.whereFilter = filterValue; + constraints?.deleteFilters(); + }, [model.source.options]); + + const props = useObjectRef({ model, resultIndex, filterValue }); const handleApply = useCallback(() => { const { model, resultIndex } = props; @@ -67,16 +65,16 @@ export const TableWhereFilter: PlaceholderComponent { - const { model, resultIndex, constraints } = props; + const { model, resultIndex } = props; + const constraints = model.source.getAction(resultIndex, ResultSetConstraintAction); if (model.isLoading() || model.isDisabled(resultIndex)) { return; } - setValue(''); + constraints.deleteDataFilters(); const applyNeeded = !!model.requestInfo.requestFilter; if (applyNeeded) { - constraints?.deleteFiltersFromConstraints(); model.refresh(); } }, []); diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index 3ddf0c8069..96bfac0c01 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -26,6 +26,7 @@ export * from './DatabaseDataModel/IDatabaseDataResult'; export * from './DatabaseDataModel/IDatabaseDataSource'; export * from './DatabaseDataModel/IDatabaseExecutionContext'; export * from './DatabaseDataModel/IDatabaseResultSet'; +export * from './DatabaseDataModel/Order'; // All Services and Components that is provided by this plugin should be exported here export * from './TableViewer/TableViewerStorageService';