@ -1,39 +1,35 @@
import * as modelUtil from 'app/client/models/modelUtil' ;
// This is circular import, but only for types so it's fine.
import type { DocModel , ViewFieldRec } from 'app/client/models/DocModel' ;
import { Style } from 'app/client/models/Styles' ;
import * as UserType from 'app/client/widgets/UserType' ;
import { ifNotSet } from 'app/common/gutil' ;
import * as ko from 'knockout' ;
import intersection from "lodash/intersection" ;
import isEqual from "lodash/isEqual" ;
import zip from 'lodash/zip' ;
export class ViewFieldConfig {
/** If there are multiple columns selected in the viewSection */
public multiselect : ko.Computed < boolean > ;
/** If all selected columns have the same widget list. */
public sameWidgets : ko.Computed < boolean > ;
/** Widget options for a field or multiple fields */
/** Widget options for a field or multiple fields . Doesn't contain style options */
public options : CommonOptions ;
/** Style options for a field or multiple fields */
public style : ko.Computed < StyleOptions > ;
// Rest of the options mimic the same options from ViewFieldRec.
public wrap : modelUtil.KoSaveableObservable < boolean | undefined > ;
public widget : ko.Computed < string | undefined > ;
public alignment : modelUtil.KoSaveableObservable < string | undefined > ;
public textColor : modelUtil.KoSaveableObservable < string | undefined > ;
public fillColor : modelUtil.KoSaveableObservable < string | undefined > ;
public fontBold : modelUtil.KoSaveableObservable < boolean | undefined > ;
public fontUnderline : modelUtil.KoSaveableObservable < boolean | undefined > ;
public fontItalic : modelUtil.KoSaveableObservable < boolean | undefined > ;
public fontStrikethrough : modelUtil.KoSaveableObservable < boolean | undefined > ;
private _fields : ko.PureComputed < ViewFieldRec [ ] > ;
public fields : ko.PureComputed < ViewFieldRec [ ] > ;
constructor ( private _field : ViewFieldRec , private _docModel : DocModel ) {
// Everything here will belong to a _field, this class is just a builder.
const owner = _field ;
// Get all selected fields from the viewSection, if there is only one field
// selected (or the selection is empty) return it in an array.
this . _ fields = owner . autoDispose ( ko . pureComputed ( ( ) = > {
this . fields = owner . autoDispose ( ko . pureComputed ( ( ) = > {
const list = this . _field . viewSection ( ) . selectedFields ( ) ;
if ( ! list || ! list . length ) {
return [ _field ] ;
@ -46,13 +42,13 @@ export class ViewFieldConfig {
} ) ) ;
// Just a helper field to see if we have multiple selected columns or not.
this . multiselect = owner . autoDispose ( ko . pureComputed ( ( ) = > this . _ fields( ) . length > 1 ) ) ;
this . multiselect = owner . autoDispose ( ko . pureComputed ( ( ) = > this . fields( ) . length > 1 ) ) ;
// Calculate if all columns share the same allowed widget list (like for Numeric type
// we have normal TextBox and Spinner). This will be used to allow the user to change
// this type if such columns are selected.
this . sameWidgets = owner . autoDispose ( ko . pureComputed ( ( ) = > {
const list = this . _ fields( ) ;
const list = this . fields( ) ;
// If we have only one field selected, list is always the same.
if ( list . length <= 1 ) { return true ; }
// Now get all widget list and calculate intersection of the Sets.
@ -73,7 +69,7 @@ export class ViewFieldConfig {
}
// If all have the same value, return it, otherwise
// return a default value for this option "undefined"
const values = this . _ fields( ) . map ( f = > f . widget ( ) ) ;
const values = this . fields( ) . map ( f = > f . widget ( ) ) ;
if ( allSame ( values ) ) {
return values [ 0 ] ;
} else {
@ -82,7 +78,7 @@ export class ViewFieldConfig {
} ,
write : ( widget ) = > {
// Go through all the fields, and reset them all.
for ( const field of this . _ fields. peek ( ) ) {
for ( const field of this . fields. peek ( ) ) {
// Reset the entire JSON, so that all options revert to their defaults.
const previous = field . widgetOptionsJson . peek ( ) ;
// We don't need to bundle anything (actions send in the same tick, are bundled
@ -102,7 +98,7 @@ export class ViewFieldConfig {
// We will use this, to know which options are allowed to be changed
// when multiple columns are selected.
const commonOptions = owner . autoDispose ( ko . pureComputed ( ( ) = > {
const fields = this . _ fields( ) ;
const fields = this . fields( ) ;
// Put all options of first widget in the Set, and then remove
// them one by one, if they are not present in other fields.
let options : Set < string > | null = null ;
@ -122,18 +118,7 @@ export class ViewFieldConfig {
}
}
}
// Add cell style options, as they are common to all widgets.
const result = options ? ? new Set ( ) ;
result . add ( 'textColor' ) ;
result . add ( 'fillColor' ) ;
result . add ( 'fontBold' ) ;
result . add ( 'fontItalic' ) ;
result . add ( 'fontUnderline' ) ;
result . add ( 'fontStrikethrough' ) ;
result . add ( 'rulesOptions' ) ;
// We are leaving rules out for this moment, as this is not supported
// at this moment.
return result ;
return options ? ? new Set ( ) ;
} ) ) ;
// Prepare our "multi" widgetOptionsJson, that can read and save
@ -147,7 +132,7 @@ export class ViewFieldConfig {
// Assemble final json object.
const result : any = { } ;
// First get all widgetOption jsons from all columns/fields.
const optionList = this . _ fields( ) . map ( f = > f . widgetOptionsJson ( ) ) ;
const optionList = this . fields( ) . map ( f = > f . widgetOptionsJson ( ) ) ;
// And fill only those that are common
const common = commonOptions ( ) ;
for ( const key of common ) {
@ -173,9 +158,9 @@ export class ViewFieldConfig {
delete value [ key ] ;
}
}
// Now update all options, for all fields, b e amending the options
// Now update all options, for all fields, b y amending the options
// object from the field/column.
for ( const item of this . _ fields. peek ( ) ) {
for ( const item of this . fields. peek ( ) ) {
const previous = item . widgetOptionsJson . peek ( ) ;
setter ( item . widgetOptionsJson , {
. . . previous ,
@ -189,10 +174,10 @@ export class ViewFieldConfig {
this . options = owner . autoDispose ( extendObservable ( modelUtil . objObservable ( options ) , {
// Property is not supported by set of columns if it is not a common option.
disabled : prop = > ko . pureComputed ( ( ) = > ! commonOptions ( ) . has ( prop ) ) ,
// Property has mixed value, if no all options are the same.
mixed : prop = > ko . pureComputed ( ( ) = > ! allSame ( this . _ fields( ) . map ( f = > f . widgetOptionsJson . prop ( prop ) ( ) ) ) ) ,
// Property has mixed value, if no t all options are the same.
mixed : prop = > ko . pureComputed ( ( ) = > ! allSame ( this . fields( ) . map ( f = > f . widgetOptionsJson . prop ( prop ) ( ) ) ) ) ,
// Property has empty value, if all options are empty (are null, undefined, empty Array or empty Object).
empty : prop = > ko . pureComputed ( ( ) = > allEmpty ( this . _ fields( ) . map ( f = > f . widgetOptionsJson . prop ( prop ) ( ) ) ) ) ,
empty : prop = > ko . pureComputed ( ( ) = > allEmpty ( this . fields( ) . map ( f = > f . widgetOptionsJson . prop ( prop ) ( ) ) ) ) ,
} ) ) ;
// This is repeated logic for wrap property in viewFieldRec,
@ -202,15 +187,74 @@ export class ViewFieldConfig {
( ) = > this . _field . viewSection ( ) . parentKey ( ) !== 'record'
) ;
// Some options extracted from our "virtual" widgetOptions json. Just to make
// them easier to use in the rest of the app.
this . alignment = this . options . prop ( 'alignment' ) ;
this . textColor = this . options . prop ( 'textColor' ) ;
this . fillColor = this . options . prop ( 'fillColor' ) ;
this . fontBold = this . options . prop ( 'fontBold' ) ;
this . fontUnderline = this . options . prop ( 'fontUnderline' ) ;
this . fontItalic = this . options . prop ( 'fontItalic' ) ;
this . fontStrikethrough = this . options . prop ( 'fontStrikethrough' ) ;
// Style options are a bit different, as they are saved when style picker is disposed.
// By the time it happens, fields may have changed (since user might have clicked some other column).
// To support this use case we need to compute a snapshot of fields, and use it to save style. Style
// picker will be rebuild every time fields change, and it will have access to last selected fields
// when it will be disposed.
this . style = ko . pureComputed ( ( ) = > {
const fields = this . fields ( ) ;
const multiSelect = fields . length > 1 ;
const savableOptions = modelUtil . savingComputed ( {
read : ( ) = > {
// For one column, just proxy this to the field.
if ( ! multiSelect ) {
return this . _field . widgetOptionsJson ( ) ;
}
// Assemble final json object.
const result : any = { } ;
// First get all widgetOption jsons from all columns/fields.
const optionList = fields . map ( f = > f . widgetOptionsJson ( ) ) ;
// And fill only those that are common
for ( const key of [ 'textColor' , 'fillColor' , 'fontBold' ,
'fontItalic' , 'fontUnderline' , 'fontStrikethrough' ] ) {
// Setting null means that this options is there, but has no value.
result [ key ] = null ;
// If all columns have the same value, use it.
if ( allSame ( optionList . map ( v = > v [ key ] ) ) ) {
result [ key ] = optionList [ 0 ] [ key ] ? ? null ;
}
}
return result ;
} ,
write : ( setter , value ) = > {
if ( ! multiSelect ) {
return setter ( this . _field . widgetOptionsJson , value ) ;
}
// When the creator panel is saving widgetOptions, it will pass
// our virtual widgetObject, which has nulls for mixed values.
// If this option wasn't changed (set), we don't want to save it.
value = { . . . value } ;
for ( const key of Object . keys ( value ) ) {
if ( value [ key ] === null ) {
delete value [ key ] ;
}
}
// Now update all options, for all fields, by amending the options
// object from the field/column.
for ( const item of fields ) {
const previous = item . widgetOptionsJson . peek ( ) ;
setter ( item . widgetOptionsJson , {
. . . previous ,
. . . value ,
} ) ;
}
}
} ) ;
// Style picker needs to be able revert to previous value, if user cancels.
const state = fields . map ( f = > f . style . peek ( ) ) ;
// We need some additional information about each property.
const result : StyleOptions = extendObservable ( modelUtil . objObservable ( savableOptions ) , {
// Property has mixed value, if not all options are the same.
mixed : prop = > ko . pureComputed ( ( ) = > ! allSame ( fields . map ( f = > f . widgetOptionsJson . prop ( prop ) ( ) ) ) ) ,
// Property has empty value, if all options are empty (are null, undefined, empty Array or empty Object).
empty : prop = > ko . pureComputed ( ( ) = > allEmpty ( fields . map ( f = > f . widgetOptionsJson . prop ( prop ) ( ) ) ) ) ,
} ) ;
result . revert = ( ) = > { zip ( fields , state ) . forEach ( ( [ f , s ] ) = > f ! . style ( s ! ) ) ; } ;
return result ;
} ) ;
}
// Helper for Choice/ChoiceList columns, that saves widget options and renames values in a document
@ -220,7 +264,7 @@ export class ViewFieldConfig {
const tableId = this . _field . column . peek ( ) . table . peek ( ) . tableId . peek ( ) ;
if ( this . multiselect . peek ( ) ) {
this . _field . config . options . update ( options ) ;
const colIds = this . _ fields. peek ( ) . map ( f = > f . colId . peek ( ) ) ;
const colIds = this . fields. peek ( ) . map ( f = > f . colId . peek ( ) ) ;
return this . _docModel . docData . bundleActions ( "Update choices configuration" , ( ) = > Promise . all ( [
this . _field . config . options . save ( ) ,
! hasRenames ? null : this . _docModel . docData . sendActions (
@ -241,26 +285,6 @@ export class ViewFieldConfig {
}
}
// Two helper methods, to support reverting viewFields style on the style
// picker. Style picker is reverting options by remembering what was
// there previously, and setting it back when user presses the cancel button.
// This won't work for mixed values, as there is no previous single value.
// To support this reverting mechanism, we will remember all styles for
// selected fields, and revert them ourselves. Style picker will either use
// our methods or fallback with its default behavior.
public copyStyles() {
return this . _fields . peek ( ) . map ( f = > f . style . peek ( ) ) ;
}
public setStyles ( styles : Style [ ] | null ) {
if ( ! styles ) {
return ;
}
for ( let i = 0 ; i < this . _fields . peek ( ) . length ; i ++ ) {
const f = this . _fields . peek ( ) [ i ] ;
f . style ( styles [ i ] ) ;
}
}
}
/ * *
@ -284,18 +308,31 @@ function allEmpty(arr: any[]) {
return arr . every ( item = > ifNotSet ( item , null ) === null ) ;
}
/ * *
* Extended version of widget options observable that contains information about mixed and empty values .
* /
type CommonOptions = modelUtil . SaveableObjObservable < any > & {
disabled ( prop : string ) : ko . Computed < boolean > ,
mixed ( prop : string ) : ko . Computed < boolean > ,
empty ( prop : string ) : ko . Computed < boolean > ,
}
/ * *
* Extended version of widget options observable that contains information about mixed and empty styles , and supports
* reverting to a previous value .
* /
type StyleOptions = modelUtil . SaveableObjObservable < any > & {
mixed ( prop : string ) : ko . Computed < boolean > ,
empty ( prop : string ) : ko . Computed < boolean > ,
revert ( ) : void ;
}
// This is helper that adds disabled computed to an ObjObservable, it follows
// the same pattern as `prop` helper.
function extendObservable (
obs : modelUtil.SaveableObjObservable < any > ,
options : { [ key : string ] : ( prop : string ) = > ko . PureComputed < boolean > }
) : CommonOptions {
) {
const result = obs as any ;
for ( const key of Object . keys ( options ) ) {
const cacheKey = ` __ ${ key } ` ;