@ -1,30 +1,30 @@
import { allCommands } from 'app/client/components/commands' ;
import { allCommands } from 'app/client/components/commands' ;
import { GristDoc } from 'app/client/components/GristDoc' ;
import { GristDoc } from 'app/client/components/GristDoc' ;
import { makeTestId } from 'app/client/lib/domUtils' ;
import * as kf from 'app/client/lib/koForm' ;
import * as kf from 'app/client/lib/koForm' ;
import { makeT } from 'app/client/lib/localization' ;
import { makeT } from 'app/client/lib/localization' ;
import { ColumnToMapImpl } from 'app/client/models/ColumnToMap' ;
import { ColumnToMapImpl } from 'app/client/models/ColumnToMap' ;
import { ColumnRec , ViewSectionRec } from 'app/client/models/DocModel' ;
import { ColumnRec , ViewSectionRec } from 'app/client/models/DocModel' ;
import { reportError } from 'app/client/models/errors' ;
import { reportError } from 'app/client/models/errors' ;
import { cssHelp , cssLabel , cssRow , cssSeparator } from 'app/client/ui/RightPanelStyles' ;
import { cssHelp , cssLabel , cssRow , cssSeparator } from 'app/client/ui/RightPanelStyles' ;
import { hoverTooltip } from 'app/client/ui/tooltips' ;
import { cssDragRow , cssFieldEntry , cssFieldLabel } from 'app/client/ui/VisibleFieldsConfig' ;
import { cssDragRow , cssFieldEntry , cssFieldLabel } from 'app/client/ui/VisibleFieldsConfig' ;
import { basicButton , primaryButton , textButton } from 'app/client/ui2018/buttons' ;
import { basicButton , primaryButton , textButton } from 'app/client/ui2018/buttons' ;
import { theme , vars } from 'app/client/ui2018/cssVars' ;
import { theme , vars } from 'app/client/ui2018/cssVars' ;
import { cssDragger } from 'app/client/ui2018/draggableList' ;
import { cssDragger } from 'app/client/ui2018/draggableList' ;
import { textInput } from 'app/client/ui2018/editableLabel' ;
import { textInput } from 'app/client/ui2018/editableLabel' ;
import { IconName } from 'app/client/ui2018/IconList' ;
import { icon } from 'app/client/ui2018/icons' ;
import { icon } from 'app/client/ui2018/icons' ;
import { cssLink } from 'app/client/ui2018/links' ;
import { cssLink } from 'app/client/ui2018/links' ;
import { IOptionFull, menu , menuItem , menuText , select } from 'app/client/ui2018/menus' ;
import { cssOptionLabel, IOption , IOptionFull, menu , menuItem , menuText , select } from 'app/client/ui2018/menus' ;
import { AccessLevel , ICustomWidget , isSatisfied } from 'app/common/CustomWidget' ;
import { AccessLevel , ICustomWidget , isSatisfied } from 'app/common/CustomWidget' ;
import { GristLoadConfig } from 'app/common/gristUrls' ;
import { GristLoadConfig } from 'app/common/gristUrls' ;
import { unwrap} from 'app/common/gutil' ;
import { not, unwrap} from 'app/common/gutil' ;
import {
import {
bundleChanges ,
bundleChanges ,
Computed ,
Computed ,
Disposable ,
Disposable ,
dom ,
dom ,
fromKo ,
fromKo ,
makeTestId ,
MultiHolder ,
MultiHolder ,
Observable ,
Observable ,
styled ,
styled ,
@ -62,12 +62,50 @@ class ColumnPicker extends Disposable {
const value = use ( this . _value ) ;
const value = use ( this . _value ) ;
return Array . isArray ( value ) ? null : value ;
return Array . isArray ( value ) ? null : value ;
} ) ;
} ) ;
properValue . onWrite ( value = > this . _value . set ( value ) ) ;
properValue . onWrite ( value = > this . _value . set ( value || null ) ) ;
const options = Computed . create ( this , use = > {
const canBeMapped = Computed . create ( this , use = > {
return use ( this . _section . columns )
return use ( this . _section . columns )
. filter ( col = > this . _column . canByMapped ( use ( col . pureType ) ) )
. filter ( col = > this . _column . canByMapped ( use ( col . pureType ) ) ) ;
. map ( ( col ) = > ( { value : col.getRowId ( ) , label : use ( col . label ) , icon : 'FieldColumn' as IconName } ) ) ;
} ) ;
// This is a HACK, to refresh options only when the menu is opened (or closed)
// and not to track down all the dependencies. Otherwise the select menu won't
// be hidden when option is selected - there is a bug that prevents it from closing
// when list of options is changed.
const refreshTrigger = Observable . create ( this , false ) ;
const options = Computed . create ( this , use = > {
void use ( refreshTrigger ) ;
const columnsAsOptions : IOption < number | null > [ ] = use ( canBeMapped )
. map ( ( col ) = > ( {
value : col.getRowId ( ) ,
label : col.label.peek ( ) ,
icon : 'FieldColumn' ,
} ) ) ;
// For optional mappings, add 'Blank' option but only if the value is set.
// This option will allow to clear the selection.
if ( this . _column . optional && properValue . get ( ) ) {
columnsAsOptions . push ( {
value : 0 ,
// Another hack. Select doesn't allow to have different label for blank option and the default text.
// So we will render this label ourselves later using `renderOptionArgs`.
label : '' ,
} ) ;
}
return columnsAsOptions ;
} ) ;
const isDisabled = Computed . create ( this , use = > {
return use ( canBeMapped ) . length === 0 ;
} ) ;
} ) ;
const defaultLabel = this . _column . typeDesc != "any"
? t ( "Pick a {{columnType}} column" , { "columnType" : this . _column . typeDesc } )
: t ( "Pick a column" ) ;
return [
return [
cssLabel (
cssLabel (
this . _column . title ,
this . _column . title ,
@ -78,18 +116,49 @@ class ColumnPicker extends Disposable {
this . _column . description ,
this . _column . description ,
testId ( 'help-for-' + this . _column . name ) ,
testId ( 'help-for-' + this . _column . name ) ,
) : null ,
) : null ,
cssRow (
dom . maybe ( not ( isDisabled ) , ( ) = > [
select (
cssRow (
properValue ,
dom . update (
options ,
select (
{
properValue ,
defaultLabel : this._column.typeDesc != "any"
options ,
? t ( "Pick a {{columnType}} column" , { "columnType" : this . _column . typeDesc } )
{
: t ( "Pick a column" )
defaultLabel ,
}
renderOptionArgs : ( opt ) = > {
) ,
// If there is a label, render it.
testId ( 'mapping-for-' + this . _column . name ) ,
// Otherwise show the 'Clear selection' label as a greyed out text.
) ,
// This is the continuation of the hack from above - were we added an option
// without a label.
return ( opt . label ) ? null : [
cssBlank ( t ( "Clear selection" ) ) ,
testId ( 'clear-selection' ) ,
] ;
}
}
) ,
dom . on ( 'click' , ( ) = > {
// When the menu is opened or closed, refresh the options.
refreshTrigger . set ( ! refreshTrigger . get ( ) ) ;
} )
) ,
testId ( 'mapping-for-' + this . _column . name ) ,
testId ( 'enabled' ) ,
) ,
] ) ,
dom . maybe ( isDisabled , ( ) = > [
cssRow (
cssDisabledSelect (
Observable . create ( this , null ) ,
[ ] , {
disabled : true ,
defaultLabel : t ( "No {{columnType}} columns in table." , { "columnType" : this . _column . typeDesc } )
}
) ,
hoverTooltip ( t ( "No {{columnType}} columns in table." , { "columnType" : this . _column . typeDesc } ) ) ,
testId ( 'mapping-for-' + this . _column . name ) ,
testId ( 'disabled' ) ,
) ,
] ) ,
] ;
] ;
}
}
}
}
@ -114,16 +183,30 @@ class ColumnListPicker extends Disposable {
} ) ;
} ) ;
}
}
private _buildAddColumn() {
private _buildAddColumn() {
const owner = MultiHolder . create ( null ) ;
const notMapped = Computed . create ( owner , use = > {
const value = use ( this . _value ) || [ ] ;
const mapped = ! Array . isArray ( value ) ? [ ] : value ;
return this . _section . columns ( ) . filter ( col = > ! mapped . includes ( use ( col . id ) ) ) ;
} ) ;
const typedColumns = Computed . create ( owner , use = > {
return use ( notMapped ) . filter ( this . _typeFilter ( use ) ) ;
} ) ;
return [
return [
cssRow (
cssRow (
dom . autoDispose ( owner ) ,
cssAddMapping (
cssAddMapping (
cssAddIcon ( 'Plus' ) , t ( "Add" ) + ' ' + this . _column . title ,
cssAddIcon ( 'Plus' ) , t ( "Add" ) + ' ' + this . _column . title ,
dom . cls ( 'disabled' , use = > use ( notMapped ) . length === 0 ) ,
testId ( 'disabled' , use = > use ( notMapped ) . length === 0 ) ,
menu ( ( ) = > {
menu ( ( ) = > {
const otherColumns = this . _getNotMappedColumns ( ) ;
const wrongTypeCount = notMapped . get ( ) . length - typedColumns . get ( ) . length ;
const typedColumns = otherColumns . filter ( this . _typeFilter ( ) ) ;
const wrongTypeCount = otherColumns . length - typedColumns . length ;
return [
return [
. . . typedColumns
. . . typedColumns . get ( )
. map ( ( col ) = > menuItem (
. map ( ( col ) = > menuItem (
( ) = > this . _addColumn ( col ) ,
( ) = > this . _addColumn ( col ) ,
col . label . peek ( ) ,
col . label . peek ( ) ,
@ -145,7 +228,8 @@ class ColumnListPicker extends Disposable {
}
}
// Helper method for filtering columns that can be picked by the widget.
// Helper method for filtering columns that can be picked by the widget.
private _typeFilter = ( use = unwrap ) = > ( col : ColumnRec ) = > this . _column . canByMapped ( use ( col . pureType ) ) ;
private _typeFilter = ( use = unwrap ) = > ( col : ColumnRec | null ) = >
! col ? false : this . _column . canByMapped ( use ( col . pureType ) ) ;
private _buildDraggableList ( use : UseCBOwner ) {
private _buildDraggableList ( use : UseCBOwner ) {
return dom . update ( kf . draggableList (
return dom . update ( kf . draggableList (
@ -159,12 +243,7 @@ class ColumnListPicker extends Disposable {
}
}
) , testId ( 'map-list-for-' + this . _column . name ) ) ;
) , testId ( 'map-list-for-' + this . _column . name ) ) ;
}
}
private _getNotMappedColumns ( ) : ColumnRec [ ] {
// Get all columns.
const all = this . _section . columns . peek ( ) ;
const mapped = this . _list ( ) ;
return all . filter ( col = > ! mapped . includes ( col . id . peek ( ) ) ) ;
}
private _readItems ( use : UseCBOwner ) : ColumnRec [ ] {
private _readItems ( use : UseCBOwner ) : ColumnRec [ ] {
let selectedRefs = ( use ( this . _value ) || [ ] ) as number [ ] ;
let selectedRefs = ( use ( this . _value ) || [ ] ) as number [ ] ;
// Ignore if configuration was changed from what it was saved.
// Ignore if configuration was changed from what it was saved.
@ -177,6 +256,7 @@ class ColumnListPicker extends Disposable {
// Remove any columns that are no longer there.
// Remove any columns that are no longer there.
return selectedRefs . map ( s = > columnMap . get ( s ) ! ) . filter ( c = > Boolean ( c ) ) ;
return selectedRefs . map ( s = > columnMap . get ( s ) ! ) . filter ( c = > Boolean ( c ) ) ;
}
}
private _renderItem ( use : UseCBOwner , field : ColumnRec ) : any {
private _renderItem ( use : UseCBOwner , field : ColumnRec ) : any {
return cssFieldEntry (
return cssFieldEntry (
cssFieldLabel (
cssFieldLabel (
@ -199,7 +279,7 @@ class ColumnListPicker extends Disposable {
this . _value . set ( value ) ;
this . _value . set ( value ) ;
} else {
} else {
let current = ( this . _value . get ( ) || [ ] ) as number [ ] ;
let current = ( this . _value . get ( ) || [ ] ) as number [ ] ;
// Ignore if the saved value is not a number .
// Ignore if the saved value is not a number list .
if ( ! Array . isArray ( current ) ) {
if ( ! Array . isArray ( current ) ) {
current = [ ] ;
current = [ ] ;
}
}
@ -224,8 +304,16 @@ class ColumnListPicker extends Disposable {
this . _value . set ( current . filter ( c = > c != column . id . peek ( ) ) ) ;
this . _value . set ( current . filter ( c = > c != column . id . peek ( ) ) ) ;
}
}
private _addColumn ( col : ColumnRec ) : any {
private _addColumn ( col : ColumnRec ) : any {
const current = this . _list ( ) ;
// Helper to find column model.
const model = ( id : number ) = > this . _section . columns ( ) . find ( c = > c . id . peek ( ) === id ) || null ;
// Get the list of currently mapped columns.
let current = this . _list ( ) ;
// Add new column.
current . push ( col . id . peek ( ) ) ;
current . push ( col . id . peek ( ) ) ;
// Remove those that don't exists anymore.
current = current . filter ( c = > model ( c ) ) ;
// And those with wrong type.
current = current . filter ( c = > this . _typeFilter ( ) ( model ( c ) ) ) ;
this . _value . set ( current ) ;
this . _value . set ( current ) ;
}
}
}
}
@ -551,8 +639,6 @@ export class CustomSectionConfig extends Disposable {
this . _widgets . set ( wigets ) ;
this . _widgets . set ( wigets ) ;
}
}
private _accept() {
private _accept() {
if ( this . _desiredAccess . get ( ) ) {
if ( this . _desiredAccess . get ( ) ) {
this . _currentAccess . set ( this . _desiredAccess . get ( ) ! ) ;
this . _currentAccess . set ( this . _desiredAccess . get ( ) ! ) ;
@ -629,6 +715,11 @@ const cssAddMapping = styled('div', `
color : $ { theme . controlHoverFg } ;
color : $ { theme . controlHoverFg } ;
-- icon - color : $ { theme . controlHoverFg } ;
-- icon - color : $ { theme . controlHoverFg } ;
}
}
& . disabled {
color : $ { theme . lightText } ;
-- icon - color : $ { theme . lightText } ;
pointer - events : none ;
}
` );
` );
const cssTextInput = styled ( textInput , `
const cssTextInput = styled ( textInput , `
@ -647,3 +738,11 @@ const cssTextInput = styled(textInput, `
color : $ { theme . inputPlaceholderFg } ;
color : $ { theme . inputPlaceholderFg } ;
}
}
` );
` );
const cssDisabledSelect = styled ( select , `
opacity : unset ! important ;
` );
const cssBlank = styled ( cssOptionLabel , `
-- grist - option - label - color : $ { theme . lightText } ;
` );