Add metadata to draft entry fields (#196)

* Add metadata to draft entry fields
* Do not render widget if value is null
* Pass along metadata
* Namespace queries to avoid conflict
* Query relational field on mount (for when editing entries)
* Make sure metadata is Immutable
* Added collection name  as metadata keys
This commit is contained in:
Cássio Souza 2016-12-29 17:18:24 -02:00 committed by GitHub
parent e47a12f6ec
commit ddfdc59941
12 changed files with 112 additions and 44 deletions

View File

@ -22,6 +22,7 @@ export const DRAFT_CREATE_FROM_ENTRY = 'DRAFT_CREATE_FROM_ENTRY';
export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY'; export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY';
export const DRAFT_DISCARD = 'DRAFT_DISCARD'; export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE = 'DRAFT_CHANGE'; export const DRAFT_CHANGE = 'DRAFT_CHANGE';
export const DRAFT_CHANGE_FIELD = 'DRAFT_CHANGE_FIELD';
export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST'; export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
@ -112,7 +113,7 @@ export function entryPersistFail(collection, entry, error) {
}; };
} }
export function emmptyDraftCreated(entry) { export function emptyDraftCreated(entry) {
return { return {
type: DRAFT_CREATE_EMPTY, type: DRAFT_CREATE_EMPTY,
payload: entry, payload: entry,
@ -141,6 +142,13 @@ export function changeDraft(entry) {
}; };
} }
export function changeDraftField(field, value, metadata) {
return {
type: DRAFT_CHANGE_FIELD,
payload: { field, value, metadata },
};
}
/* /*
* Exported Thunk Action Creators * Exported Thunk Action Creators
*/ */
@ -180,7 +188,7 @@ export function createEmptyDraft(collection) {
dataFields[field.get('name')] = field.get('default', null); dataFields[field.get('name')] = field.get('default', null);
}); });
const newEntry = createEntry(collection.get('name'), '', '', { data: dataFields }); const newEntry = createEntry(collection.get('name'), '', '', { data: dataFields });
dispatch(emmptyDraftCreated(newEntry)); dispatch(emptyDraftCreated(newEntry));
}; };
} }

View File

@ -9,9 +9,9 @@ export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST';
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS'; export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE'; export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
export const QUERY_REQUEST = 'QUERY_REQUEST'; export const QUERY_REQUEST = 'INIT_QUERY';
export const QUERY_SUCCESS = 'QUERY_SUCCESS'; export const QUERY_SUCCESS = 'QUERY_OK';
export const QUERY_FAILURE = 'QUERY_FAILURE'; export const QUERY_FAILURE = 'QUERY_ERROR';
export const SEARCH_CLEAR = 'SEARCH_CLEAR'; export const SEARCH_CLEAR = 'SEARCH_CLEAR';
@ -47,10 +47,11 @@ export function searchFailure(searchTerm, error) {
}; };
} }
export function querying(collection, searchFields, searchTerm) { export function querying(namespace, collection, searchFields, searchTerm) {
return { return {
type: QUERY_REQUEST, type: QUERY_REQUEST,
payload: { payload: {
namespace,
collection, collection,
searchFields, searchFields,
searchTerm, searchTerm,
@ -58,10 +59,11 @@ export function querying(collection, searchFields, searchTerm) {
}; };
} }
export function querySuccess(collection, searchFields, searchTerm, response) { export function querySuccess(namespace, collection, searchFields, searchTerm, response) {
return { return {
type: QUERY_SUCCESS, type: QUERY_SUCCESS,
payload: { payload: {
namespace,
collection, collection,
searchFields, searchFields,
searchTerm, searchTerm,
@ -70,10 +72,11 @@ export function querySuccess(collection, searchFields, searchTerm, response) {
}; };
} }
export function queryFailure(collection, searchFields, searchTerm, error) { export function queryFailure(namespace, collection, searchFields, searchTerm, error) {
return { return {
type: QUERY_SUCCESS, type: QUERY_SUCCESS,
payload: { payload: {
namespace,
collection, collection,
searchFields, searchFields,
searchTerm, searchTerm,
@ -118,20 +121,20 @@ export function searchEntries(searchTerm, page = 0) {
// Instead of searching for complete entries, query will search for specific fields // Instead of searching for complete entries, query will search for specific fields
// in specific collections and return raw data (no entries). // in specific collections and return raw data (no entries).
export function query(collection, searchFields, searchTerm) { export function query(namespace, collection, searchFields, searchTerm) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const integration = selectIntegration(state, collection, 'search'); const integration = selectIntegration(state, collection, 'search');
if (!integration) { if (!integration) {
dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); dispatch(searchFailure(namespace, searchTerm, 'Search integration is not configured.'));
} }
const provider = integration ? const provider = integration ?
getIntegrationProvider(state.integrations, integration) getIntegrationProvider(state.integrations, integration)
: currentBackend(state.config); : currentBackend(state.config);
dispatch(querying(collection, searchFields, searchTerm)); dispatch(querying(namespace, collection, searchFields, searchTerm));
provider.searchBy(searchFields, collection, searchTerm).then( provider.searchBy(searchFields, collection, searchTerm).then(
response => dispatch(querySuccess(collection, searchFields, searchTerm, response)), response => dispatch(querySuccess(namespace, collection, searchFields, searchTerm, response)),
error => dispatch(queryFailure(collection, searchFields, searchTerm, error)) error => dispatch(queryFailure(namespace, collection, searchFields, searchTerm, error))
); );
}; };
} }

View File

@ -10,10 +10,11 @@ function isHidden(field) {
export default class ControlPane extends Component { export default class ControlPane extends Component {
controlFor(field) { controlFor(field) {
const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; const { entry, fieldsMetaData, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
const widget = resolveWidget(field.get('widget')); const widget = resolveWidget(field.get('widget'));
const fieldName = field.get('name'); const fieldName = field.get('name');
const value = entry.getIn(['data', fieldName]); const value = entry.getIn(['data', fieldName]);
const metadata = fieldsMetaData.get(fieldName);
if (entry.size === 0 || entry.get('partial') === true) return null; if (entry.size === 0 || entry.get('partial') === true) return null;
return ( return (
<div className={styles.control}> <div className={styles.control}>
@ -22,7 +23,8 @@ export default class ControlPane extends Component {
React.createElement(widget.control, { React.createElement(widget.control, {
field, field,
value, value,
onChange: val => onChange(entry.setIn(['data', fieldName], val)), metadata,
onChange: (newValue, newMetadata) => onChange(fieldName, newValue, newMetadata),
onAddMedia, onAddMedia,
onRemoveMedia, onRemoveMedia,
getMedia, getMedia,
@ -57,6 +59,7 @@ ControlPane.propTypes = {
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
fieldsMetaData: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired, getMedia: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddMedia: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,

View File

@ -25,6 +25,7 @@ class EntryEditor extends Component {
collection, collection,
entry, entry,
fields, fields,
fieldsMetaData,
getMedia, getMedia,
onChange, onChange,
onAddMedia, onAddMedia,
@ -51,6 +52,7 @@ class EntryEditor extends Component {
collection={collection} collection={collection}
entry={entry} entry={entry}
fields={fields} fields={fields}
fieldsMetaData={fieldsMetaData}
getMedia={getMedia} getMedia={getMedia}
onChange={onChange} onChange={onChange}
onAddMedia={onAddMedia} onAddMedia={onAddMedia}
@ -64,6 +66,7 @@ class EntryEditor extends Component {
collection={collection} collection={collection}
entry={entry} entry={entry}
fields={fields} fields={fields}
fieldsMetaData={fieldsMetaData}
getMedia={getMedia} getMedia={getMedia}
/> />
</div> </div>
@ -87,6 +90,7 @@ EntryEditor.propTypes = {
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
fieldsMetaData: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired, getMedia: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddMedia: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,

View File

@ -29,20 +29,23 @@ export default class PreviewPane extends React.Component {
} }
widgetFor = (name) => { widgetFor = (name) => {
const { fields, entry, getMedia } = this.props; const { fields, entry, fieldsMetaData, getMedia } = this.props;
const field = fields.find(f => f.get('name') === name); const field = fields.find(f => f.get('name') === name);
let value = entry.getIn(['data', field.get('name')]); let value = entry.getIn(['data', field.get('name')]);
const metadata = fieldsMetaData.get(field.get('name'));
const labelledWidgets = ['string', 'text', 'number']; const labelledWidgets = ['string', 'text', 'number'];
if (Object.keys(this.inferedFields).indexOf(name) !== -1) { if (Object.keys(this.inferedFields).indexOf(name) !== -1) {
value = this.inferedFields[name].defaultPreview(value); value = this.inferedFields[name].defaultPreview(value);
} else if (value && labelledWidgets.indexOf(field.get('widget')) !== -1 && value.toString().length < 50) { } else if (value && labelledWidgets.indexOf(field.get('widget')) !== -1 && value.toString().length < 50) {
value = <div><strong>{field.get('label')}:</strong> {value}</div>; value = <div><strong>{field.get('label')}:</strong> {value}</div>;
} }
if (!value) return null;
const widget = resolveWidget(field.get('widget')); const widget = resolveWidget(field.get('widget'));
return React.createElement(widget.preview, { return React.createElement(widget.preview, {
key: field.get('name'), key: field.get('name'),
value, value,
field, field,
metadata,
getMedia, getMedia,
}); });
}; };
@ -100,5 +103,6 @@ PreviewPane.propTypes = {
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fieldsMetaData: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired, getMedia: PropTypes.func.isRequired,
}; };

View File

@ -83,10 +83,10 @@ export default class ListControl extends Component {
}; };
handleChangeFor(index) { handleChangeFor(index) {
return (newValue) => { return (newValue, newMetadata) => {
const { value, onChange } = this.props; const { value, onChange } = this.props;
const parsedValue = (this.valueType === valueTypes.SINGLE) ? newValue.first() : newValue; const parsedValue = (this.valueType === valueTypes.SINGLE) ? newValue.first() : newValue;
onChange(value.set(index, parsedValue)); onChange(value.set(index, parsedValue), newMetadata);
}; };
} }

View File

@ -25,8 +25,8 @@ export default class ObjectControl extends Component {
React.createElement(widget.control, { React.createElement(widget.control, {
field, field,
value: fieldValue, value: fieldValue,
onChange: (val) => { onChange: (val, metadata) => {
onChange((value || Map()).set(field.get('name'), val)); onChange((value || Map()).set(field.get('name'), val), metadata);
}, },
onAddMedia, onAddMedia,
onRemoveMedia, onRemoveMedia,

View File

@ -1,5 +1,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Autosuggest from 'react-autosuggest'; import Autosuggest from 'react-autosuggest';
import uuid from 'uuid';
import { Map } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { Loader } from '../../components/UI/index'; import { Loader } from '../../components/UI/index';
@ -15,22 +17,57 @@ class RelationControl extends Component {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
isFetching: PropTypes.bool, isFetching: PropTypes.node,
query: PropTypes.func.isRequired, query: PropTypes.func.isRequired,
clearSearch: PropTypes.func.isRequired, clearSearch: PropTypes.func.isRequired,
queryHits: PropTypes.array, // eslint-disable-line queryHits: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
]),
}; };
constructor(props, ctx) {
super(props, ctx);
this.controlID = uuid.v4();
this.didInitialSearch = false;
}
componentDidMount() {
const { value, field } = this.props;
if (value) {
const collection = field.get('collection');
const searchFields = field.get('searchFields').map(f => `data.${ f }`).toJS();
this.props.query(this.controlID, collection, searchFields, value);
}
}
componentWillReceiveProps(nextProps) {
if (this.didInitialSearch) return;
if (nextProps.queryHits !== this.props.queryHits && nextProps.queryHits.get && nextProps.queryHits.get(this.controlID)) {
this.didInitialSearch = true;
const suggestion = nextProps.queryHits.get(this.controlID);
if (suggestion && suggestion.length === 1) {
const val = this.getSuggestionValue(suggestion[0]);
this.props.onChange(val, { [nextProps.field.get('collection')]: { [val]: suggestion[0].data } });
}
}
}
onChange = (event, { newValue }) => { onChange = (event, { newValue }) => {
this.props.onChange(newValue); this.props.onChange(newValue);
}; };
onSuggestionSelected = (event, { suggestion }) => {
const value = this.getSuggestionValue(suggestion);
this.props.onChange(value, { [this.props.field.get('collection')]: { [value]: suggestion.data } });
};
onSuggestionsFetchRequested = debounce(({ value }) => { onSuggestionsFetchRequested = debounce(({ value }) => {
if (value.length < 3) return; if (value.length < 2) return;
const { field } = this.props; const { field } = this.props;
const collection = field.get('collection'); const collection = field.get('collection');
const searchFields = field.get('searchFields').map(f => `data.${ f }`).toJS(); const searchFields = field.get('searchFields').map(f => `data.${ f }`).toJS();
this.props.query(collection, searchFields, value); this.props.query(this.controlID, collection, searchFields, value);
}, 80); }, 80);
onSuggestionsClearRequested = () => { onSuggestionsClearRequested = () => {
@ -46,8 +83,7 @@ class RelationControl extends Component {
if (escapedValue === '') { if (escapedValue === '') {
return []; return [];
} }
return queryHits.get(this.controlID).filter((hit) => {
return queryHits.filter((hit) => {
let testResult = false; let testResult = false;
searchFields.forEach((f) => { searchFields.forEach((f) => {
testResult = testResult || regex.test(hit.data[f]); testResult = testResult || regex.test(hit.data[f]);
@ -77,17 +113,20 @@ class RelationControl extends Component {
onChange: this.onChange, onChange: this.onChange,
}; };
const suggestions = (queryHits.get) ? queryHits.get(this.controlID, []) : [];
return ( return (
<div> <div>
<Autosuggest <Autosuggest
suggestions={queryHits} suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} // eslint-disable-line onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested} // eslint-disable-line onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={this.getSuggestionValue} getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion} renderSuggestion={this.renderSuggestion}
inputProps={inputProps} inputProps={inputProps}
/> />
<Loader active={isFetching} /> <Loader active={isFetching === this.controlID} />
</div> </div>
); );
} }

View File

@ -6,7 +6,7 @@ import {
createDraftFromEntry, createDraftFromEntry,
createEmptyDraft, createEmptyDraft,
discardDraft, discardDraft,
changeDraft, changeDraftField,
persistEntry, persistEntry,
} from '../actions/entries'; } from '../actions/entries';
import { cancelEdit } from '../actions/editor'; import { cancelEdit } from '../actions/editor';
@ -22,7 +22,7 @@ class EntryPage extends React.Component {
static propTypes = { static propTypes = {
addMedia: PropTypes.func.isRequired, addMedia: PropTypes.func.isRequired,
boundGetMedia: PropTypes.func.isRequired, boundGetMedia: PropTypes.func.isRequired,
changeDraft: PropTypes.func.isRequired, changeDraftField: PropTypes.func.isRequired,
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
createDraftFromEntry: PropTypes.func.isRequired, createDraftFromEntry: PropTypes.func.isRequired,
createEmptyDraft: PropTypes.func.isRequired, createEmptyDraft: PropTypes.func.isRequired,
@ -79,7 +79,7 @@ class EntryPage extends React.Component {
fields, fields,
boundGetMedia, boundGetMedia,
collection, collection,
changeDraft, changeDraftField,
addMedia, addMedia,
removeMedia, removeMedia,
cancelEdit, cancelEdit,
@ -97,7 +97,8 @@ class EntryPage extends React.Component {
getMedia={boundGetMedia} getMedia={boundGetMedia}
collection={collection} collection={collection}
fields={fields} fields={fields}
onChange={changeDraft} fieldsMetaData={entryDraft.get('fieldsMetaData')}
onChange={changeDraftField}
onAddMedia={addMedia} onAddMedia={addMedia}
onRemoveMedia={removeMedia} onRemoveMedia={removeMedia}
onPersist={this.handlePersistEntry} onPersist={this.handlePersistEntry}
@ -130,7 +131,7 @@ function mapStateToProps(state, ownProps) {
export default connect( export default connect(
mapStateToProps, mapStateToProps,
{ {
changeDraft, changeDraftField,
addMedia, addMedia,
removeMedia, removeMedia,
loadEntry, loadEntry,

View File

@ -2,7 +2,7 @@ import { Map, List, fromJS } from 'immutable';
import * as actions from '../../actions/entries'; import * as actions from '../../actions/entries';
import reducer from '../entryDraft'; import reducer from '../entryDraft';
let initialState = Map({ entry: Map(), mediaFiles: List() }); let initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() });
const entry = { const entry = {
collection: 'posts', collection: 'posts',
@ -29,6 +29,7 @@ describe('entryDraft reducer', () => {
newRecord: false, newRecord: false,
}, },
mediaFiles: [], mediaFiles: [],
fieldsMetaData: Map(),
}) })
); );
}); });
@ -39,7 +40,7 @@ describe('entryDraft reducer', () => {
expect( expect(
reducer( reducer(
initialState, initialState,
actions.emmptyDraftCreated(fromJS(entry)) actions.emptyDraftCreated(fromJS(entry))
) )
).toEqual( ).toEqual(
fromJS({ fromJS({
@ -48,6 +49,7 @@ describe('entryDraft reducer', () => {
newRecord: true, newRecord: true,
}, },
mediaFiles: [], mediaFiles: [],
fieldsMetaData: Map(),
}) })
); );
}); });

View File

@ -3,7 +3,7 @@ import {
DRAFT_CREATE_FROM_ENTRY, DRAFT_CREATE_FROM_ENTRY,
DRAFT_CREATE_EMPTY, DRAFT_CREATE_EMPTY,
DRAFT_DISCARD, DRAFT_DISCARD,
DRAFT_CHANGE, DRAFT_CHANGE_FIELD,
ENTRY_PERSIST_REQUEST, ENTRY_PERSIST_REQUEST,
ENTRY_PERSIST_SUCCESS, ENTRY_PERSIST_SUCCESS,
ENTRY_PERSIST_FAILURE, ENTRY_PERSIST_FAILURE,
@ -13,7 +13,7 @@ import {
REMOVE_MEDIA, REMOVE_MEDIA,
} from '../actions/media'; } from '../actions/media';
const initialState = Map({ entry: Map(), mediaFiles: List() }); const initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() });
const entryDraftReducer = (state = Map(), action) => { const entryDraftReducer = (state = Map(), action) => {
switch (action.type) { switch (action.type) {
@ -23,6 +23,7 @@ const entryDraftReducer = (state = Map(), action) => {
state.set('entry', action.payload); state.set('entry', action.payload);
state.setIn(['entry', 'newRecord'], false); state.setIn(['entry', 'newRecord'], false);
state.set('mediaFiles', List()); state.set('mediaFiles', List());
state.set('fieldsMetaData', Map());
}); });
case DRAFT_CREATE_EMPTY: case DRAFT_CREATE_EMPTY:
// New Entry // New Entry
@ -30,12 +31,15 @@ const entryDraftReducer = (state = Map(), action) => {
state.set('entry', fromJS(action.payload)); state.set('entry', fromJS(action.payload));
state.setIn(['entry', 'newRecord'], true); state.setIn(['entry', 'newRecord'], true);
state.set('mediaFiles', List()); state.set('mediaFiles', List());
state.set('fieldsMetaData', Map());
}); });
case DRAFT_DISCARD: case DRAFT_DISCARD:
return initialState; return initialState;
case DRAFT_CHANGE: case DRAFT_CHANGE_FIELD:
return state.set('entry', action.payload); return state.withMutations((state) => {
state.setIn(['entry', 'data', action.payload.field], action.payload.value);
state.mergeIn(['fieldsMetaData'], fromJS(action.payload.metadata));
});
case ENTRY_PERSIST_REQUEST: { case ENTRY_PERSIST_REQUEST: {
return state.setIn(['entry', 'isPersisting'], true); return state.setIn(['entry', 'isPersisting'], true);
} }

View File

@ -44,7 +44,7 @@ const entries = (state = defaultState, action) => {
case QUERY_REQUEST: case QUERY_REQUEST:
if (action.payload.searchTerm !== state.get('term')) { if (action.payload.searchTerm !== state.get('term')) {
return state.withMutations((map) => { return state.withMutations((map) => {
map.set('isFetching', true); map.set('isFetching', action.payload.namespace);
map.set('term', action.payload.searchTerm); map.set('term', action.payload.searchTerm);
}); });
} }
@ -56,7 +56,7 @@ const entries = (state = defaultState, action) => {
return state.withMutations((map) => { return state.withMutations((map) => {
map.set('isFetching', false); map.set('isFetching', false);
map.set('term', searchTerm); map.set('term', searchTerm);
map.set('queryHits', response.hits); map.set('queryHits', Map({ [action.payload.namespace]: response.hits }));
}); });
default: default: