choerodon-ui
Version:
An enterprise-class UI design language and React-based implementation
772 lines (627 loc) • 28.1 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault")["default"];
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard")["default"];
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = _interopRequireWildcard(require("react"));
var _classnames = _interopRequireDefault(require("classnames"));
var _mobxReactLite = require("mobx-react-lite");
var _reactBeautifulDnd = require("react-beautiful-dnd");
var _mobx = require("mobx");
var _isString = _interopRequireDefault(require("lodash/isString"));
var _noop = _interopRequireDefault(require("lodash/noop"));
var _isObject = _interopRequireDefault(require("lodash/isObject"));
var _isFunction = _interopRequireDefault(require("lodash/isFunction"));
var _uniqWith = _interopRequireDefault(require("lodash/uniqWith"));
var _isEqual = _interopRequireDefault(require("lodash/isEqual"));
var _icon = _interopRequireDefault(require("../../../lib/icon"));
var _grid = require("../../../lib/grid");
var _dataSet = _interopRequireDefault(require("../data-set"));
var _KanbanColumn = _interopRequireDefault(require("./KanbanColumn"));
var _BoardContext = _interopRequireDefault(require("./BoardContext"));
var _TableDynamicFilterBar = _interopRequireDefault(require("../table/query-bar/TableDynamicFilterBar"));
var _utils = require("../table/utils");
var _Button = _interopRequireDefault(require("../button/Button"));
var _enum = require("../button/enum");
var _ModalProvider = require("../modal-provider/ModalProvider");
var _localeContext = require("../locale-context");
var _kanbanCustomizationSettings = _interopRequireDefault(require("./kanban-customization-settings"));
var _enum2 = require("./enum");
var _interface = require("../table/interface");
var _interface2 = require("../data-set/interface");
var _Dropdown = _interopRequireDefault(require("../dropdown/Dropdown"));
var _menu = _interopRequireDefault(require("../menu"));
var _excluded = ["afterClick"],
_excluded2 = ["afterClick"];
var KanbanContent = function KanbanContent(props) {
var dataSet = props.dataSet,
buttons = props.buttons,
_props$kanbanProps = props.kanbanProps,
kanbanProps = _props$kanbanProps === void 0 ? {} : _props$kanbanProps,
tableBtns = props.tableBtns,
buttonsLimit = props.buttonsLimit,
queryBarProps = props.queryBarProps;
var _useContext = (0, _react.useContext)(_BoardContext["default"]),
onChange = _useContext.onChange,
getConfig = _useContext.getConfig,
getProPrefixCls = _useContext.getProPrefixCls,
_useContext$prefixCls = _useContext.prefixCls,
prefixCls = _useContext$prefixCls === void 0 ? '' : _useContext$prefixCls,
customizedDS = _useContext.customizedDS,
queryFields = _useContext.queryFields,
autoQuery = _useContext.autoQuery,
_useContext$renderBut = _useContext.renderButtons,
renderButtons = _useContext$renderBut === void 0 ? _noop["default"] : _useContext$renderBut;
var _useState = (0, _react.useState)(false),
_useState2 = (0, _slicedToArray2["default"])(_useState, 2),
loaded = _useState2[0],
setLoaded = _useState2[1];
var _useState3 = (0, _react.useState)(),
_useState4 = (0, _slicedToArray2["default"])(_useState3, 2),
columnData = _useState4[0],
setColumnData = _useState4[1];
var groupField = customizedDS.current.get(_enum2.ViewField.groupField);
var viewProps = customizedDS.current.get(_enum2.ViewField.viewProps);
var dataKey = kanbanProps.columnDsProps ? kanbanProps.columnDsProps.dataKey || getConfig('dataKey') : getConfig('dataKey');
var totalKey = kanbanProps.columnDsProps ? kanbanProps.columnDsProps.totalKey || getConfig('totalKey') : getConfig('dataKey');
var dsField = (0, _react.useMemo)(function () {
return dataSet.fields.get(groupField);
}, [dataSet, groupField]);
var modal = (0, _ModalProvider.useModal)();
var openCustomizationModal = (0, _react.useCallback)(function () {
var modalProps = {
drawer: true,
title: '看板视图配置',
children: /*#__PURE__*/_react["default"].createElement(_kanbanCustomizationSettings["default"], {
viewMode: _enum2.ViewMode.kanban
}),
bodyStyle: {
overflow: 'hidden auto',
padding: 0
}
};
modalProps.okText = (0, _localeContext.$l)('Tabs', 'save');
modal.open(modalProps);
}, [modal]);
var kanbanDS = (0, _react.useMemo)(function () {
var defaultSortParams = dataSet.combineSort && viewProps ? viewProps[_enum2.ViewField.combineSort] || [] : [];
var orgFields = dataSet.props.fields ? dataSet.props.fields : [];
var orderFields = orgFields.map(function (field) {
var orderField = defaultSortParams.find(function (of) {
return of.sortName === field.name;
});
var newField = (0, _objectSpread2["default"])({}, field);
if (orderField) {
newField.order = orderField.order;
}
return newField;
});
return new _dataSet["default"]((0, _objectSpread2["default"])((0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, dataSet.props), {}, {
dataKey: 'null',
fields: orderFields
}, kanbanProps.allDsProps), {}, {
autoQuery: false
}));
}, []);
var loadColumnData = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
var oldId, isChanged, res, changed, _res, _changed;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
setLoaded(false); // 处理看板视图初始化onChange问题
oldId = customizedDS && customizedDS.getState('__OLDID__');
isChanged = !(0, _isEqual["default"])(oldId, customizedDS ? customizedDS.current.get(_enum2.ViewField.id) : undefined);
if (!(dsField && dsField.getOptions() && dsField.getOptions().toData().length)) {
_context.next = 17;
break;
}
res = (0, _toConsumableArray2["default"])(dsField.getOptions().toData());
res.unshift({});
setColumnData((0, _uniqWith["default"])(res, _isEqual["default"]));
kanbanDS.setQueryParameter('groupingBy', dsField.name);
dataSet.setState('__CURRENTVIEWDS__', kanbanDS);
changed = customizedDS && customizedDS.getState('__ISCHANGE__');
if ((0, _isFunction["default"])(onChange) && changed && isChanged) {
onChange({
dataSet: dataSet,
currentViewDS: kanbanDS,
record: customizedDS.current
});
}
if (!autoQuery) {
_context.next = 14;
break;
}
_context.next = 14;
return kanbanDS.query();
case 14:
setLoaded(true);
_context.next = 36;
break;
case 17:
if (!dsField) {
_context.next = 36;
break;
}
_context.prev = 18;
_context.next = 21;
return dsField.fetchLookup(true);
case 21:
_res = _context.sent;
if (!(_res && _res.length)) {
_context.next = 33;
break;
}
_res.unshift({});
setColumnData((0, _uniqWith["default"])(_res, _isEqual["default"]));
kanbanDS.setQueryParameter('groupingBy', dsField.name);
dataSet.setState('__CURRENTVIEWDS__', kanbanDS);
_changed = customizedDS && customizedDS.getState('__ISCHANGE__');
if ((0, _isFunction["default"])(onChange) && _changed && isChanged) {
onChange({
dataSet: dataSet,
currentViewDS: kanbanDS,
record: customizedDS.current
});
}
if (!autoQuery) {
_context.next = 32;
break;
}
_context.next = 32;
return kanbanDS.query();
case 32:
setLoaded(true);
case 33:
_context.prev = 33;
setLoaded(true);
return _context.finish(33);
case 36:
case "end":
return _context.stop();
}
}
}, _callee, null, [[18,, 33, 36]]);
})), [dsField, dataSet]);
(0, _react.useEffect)(function () {
loadColumnData();
}, [dataSet, customizedDS.current, groupField, kanbanProps]);
/**
* 根据列数据渲染列组件
* @returns 看板列组件
*/
var getBoardColumns = (0, _react.useCallback)(function () {
if (dsField && columnData && loaded) {
var groupFieldName = dsField.get('name');
var groupValueField = dsField.get('valueField');
var groupTextField = dsField.get('textField');
return columnData.map(function (groupRecord) {
var groupValue = groupRecord[groupValueField];
var groupText = groupRecord[groupTextField];
var kanbanQuote = kanbanDS.find(function (record) {
return record.get(groupFieldName) === groupValue;
});
kanbanDS.setState("".concat(groupValue === undefined ? '_empty' : groupValue, "_totalCount"), kanbanQuote ? kanbanQuote.get(totalKey) : undefined);
return /*#__PURE__*/_react["default"].createElement(_KanbanColumn["default"], {
key: groupValue === undefined ? '_empty' : groupValue,
kanbanProps: kanbanProps,
prefixCls: prefixCls,
quotes: kanbanQuote ? kanbanQuote.get(dataKey) : [],
kanbanDS: kanbanDS,
columnId: groupValue === undefined ? '_empty' : groupValue,
header: groupText || '未分组',
groupingBy: groupFieldName
});
});
}
return null;
}, [columnData, loaded]);
var cls = (0, _classnames["default"])("".concat(prefixCls, "-wrapper"), {});
/**
* buttons 大于 buttonsLimits 放入下拉
* @param buttonsLimits
*/
var getMoreButton = function getMoreButton(buttonsLimits) {
var tableButtonProps = getConfig('tableButtonProps');
var children = [];
if (tableBtns && tableBtns.length && buttonsLimits) {
tableBtns.slice(buttonsLimits).forEach(function (button) {
var props = {};
if ((0, _mobx.isArrayLike)(button)) {
props = button[1] || {};
button = button[0];
}
if ((0, _isString["default"])(button) && button in _interface.TableButtonType) {
var _props = props,
afterClick = _props.afterClick,
buttonProps = (0, _objectWithoutProperties2["default"])(_props, _excluded);
var defaultButtonProps = getButtonProps(button);
if (defaultButtonProps) {
if (afterClick) {
var onClick = defaultButtonProps.onClick;
defaultButtonProps.onClick = /*#__PURE__*/function () {
var _ref2 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(e) {
return _regenerator["default"].wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
e.persist();
_context2.prev = 1;
_context2.next = 4;
return onClick(e);
case 4:
_context2.prev = 4;
afterClick(e);
return _context2.finish(4);
case 7:
case "end":
return _context2.stop();
}
}
}, _callee2, null, [[1,, 4, 7]]);
}));
return function (_x) {
return _ref2.apply(this, arguments);
};
}();
}
children.push( /*#__PURE__*/_react["default"].createElement(_menu["default"].Item, {
hidden: tableButtonProps.hidden,
key: button
}, /*#__PURE__*/_react["default"].createElement(_Button["default"], (0, _extends2["default"])({
key: "".concat(button, "-btn")
}, tableButtonProps, defaultButtonProps, buttonProps, {
funcType: _enum.FuncType.link
}))));
}
} else if ( /*#__PURE__*/(0, _react.isValidElement)(button)) {
children.push( /*#__PURE__*/_react["default"].createElement(_menu["default"].Item, {
hidden: button.props.hidden
}, /*#__PURE__*/(0, _react.cloneElement)(button, (0, _objectSpread2["default"])((0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, tableButtonProps), button.props), {}, {
funcType: _enum.FuncType.link
}))));
} else if ((0, _isObject["default"])(button)) {
children.push( /*#__PURE__*/_react["default"].createElement(_menu["default"].Item, {
hidden: props.hidden
}, /*#__PURE__*/_react["default"].createElement(_Button["default"], (0, _extends2["default"])({}, tableButtonProps, button, {
funcType: _enum.FuncType.link
}))));
}
});
}
var menu = /*#__PURE__*/_react["default"].createElement(_menu["default"], {
prefixCls: "".concat(getProPrefixCls('table'), "-dropdown-menu")
}, children);
return /*#__PURE__*/_react["default"].createElement(_Dropdown["default"], {
overlay: menu,
key: "dropdown_button"
}, /*#__PURE__*/_react["default"].createElement(_Button["default"], (0, _extends2["default"])({}, tableButtonProps, {
key: "more_button"
}), /*#__PURE__*/_react["default"].createElement("span", null, (0, _localeContext.$l)('Table', 'more')), /*#__PURE__*/_react["default"].createElement(_icon["default"], {
type: 'expand_more'
})));
};
var getButtonProps = (0, _react.useCallback)(function (type) {
var disabled = kanbanDS.status !== _interface2.DataSetStatus.ready;
switch (type) {
case _interface.TableButtonType.add:
return {
icon: 'playlist_add',
onClick: function onClick() {
return kanbanDS.create({}, 0);
},
children: (0, _localeContext.$l)('Table', 'create_button'),
disabled: disabled || (kanbanDS.parent ? !kanbanDS.parent.current : false)
};
case _interface.TableButtonType.save:
return {
icon: 'save',
onClick: function onClick() {
return kanbanDS.submit();
},
children: (0, _localeContext.$l)('Table', 'save_button'),
type: _enum.ButtonType.submit,
disabled: disabled
};
case _interface.TableButtonType["delete"]:
return {
icon: 'delete',
onClick: function onClick() {
return kanbanDS["delete"](kanbanDS.selected);
},
children: (0, _localeContext.$l)('Table', 'delete_button'),
disabled: disabled || kanbanDS.selected.length === 0
};
case _interface.TableButtonType.remove:
return {
icon: 'remove_circle',
onClick: function onClick() {
return kanbanDS.remove(kanbanDS.selected);
},
children: (0, _localeContext.$l)('Table', 'remove_button'),
disabled: disabled || kanbanDS.selected.length === 0
};
case _interface.TableButtonType.reset:
return {
icon: 'undo',
onClick: function onClick() {
return kanbanDS.reset();
},
children: (0, _localeContext.$l)('Table', 'reset_button'),
type: _enum.ButtonType.reset
};
case _interface.TableButtonType.query:
return {
icon: 'search',
onClick: function () {
var _onClick = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee3() {
return _regenerator["default"].wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.next = 2;
return kanbanDS.modifiedCheck(undefined, dataSet, 'query');
case 2:
_context3.t2 = _context3.sent;
if (!_context3.t2) {
_context3.next = 5;
break;
}
_context3.t2 = kanbanDS.queryDataSet;
case 5:
_context3.t1 = _context3.t2;
if (!_context3.t1) {
_context3.next = 8;
break;
}
_context3.t1 = kanbanDS.queryDataSet.current;
case 8:
_context3.t0 = _context3.t1;
if (!_context3.t0) {
_context3.next = 13;
break;
}
_context3.next = 12;
return kanbanDS.queryDataSet.current.validate();
case 12:
_context3.t0 = _context3.sent;
case 13:
if (!_context3.t0) {
_context3.next = 15;
break;
}
return _context3.abrupt("return", kanbanDS.query());
case 15:
case "end":
return _context3.stop();
}
}
}, _callee3);
}));
function onClick() {
return _onClick.apply(this, arguments);
}
return onClick;
}(),
children: (0, _localeContext.$l)('Table', 'query_button')
};
case _interface.TableButtonType["export"]:
return undefined;
case _interface.TableButtonType.expandAll:
return undefined;
case _interface.TableButtonType.collapseAll:
return undefined;
default:
}
}, []);
var getButtons = (0, _react.useCallback)(function (buttonsArr) {
var children = [];
var buttons = buttonsArr || tableBtns;
if (buttons) {
var tableButtonProps = getConfig('tableButtonProps');
var _buttonsArr = buttons.slice(0, buttonsLimit);
_buttonsArr.forEach(function (button) {
var props = {};
if ((0, _mobx.isArrayLike)(button)) {
props = button[1] || {};
button = button[0];
}
if ((0, _isString["default"])(button) && button in _interface.TableButtonType) {
var _props2 = props,
afterClick = _props2.afterClick,
buttonProps = (0, _objectWithoutProperties2["default"])(_props2, _excluded2);
var defaultButtonProps = getButtonProps(button);
if (defaultButtonProps) {
if (afterClick) {
var onClick = defaultButtonProps.onClick;
defaultButtonProps.onClick = /*#__PURE__*/function () {
var _ref3 = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee4(e) {
return _regenerator["default"].wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
e.persist();
_context4.prev = 1;
_context4.next = 4;
return onClick(e);
case 4:
_context4.prev = 4;
afterClick(e);
return _context4.finish(4);
case 7:
case "end":
return _context4.stop();
}
}
}, _callee4, null, [[1,, 4, 7]]);
}));
return function (_x2) {
return _ref3.apply(this, arguments);
};
}();
}
children.push( /*#__PURE__*/_react["default"].createElement(_Button["default"], (0, _extends2["default"])({
key: button
}, tableButtonProps, defaultButtonProps, buttonProps)));
}
} else if ( /*#__PURE__*/(0, _react.isValidElement)(button)) {
children.push( /*#__PURE__*/(0, _react.cloneElement)(button, (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, tableButtonProps), button.props)));
} else if ((0, _isObject["default"])(button)) {
children.push( /*#__PURE__*/_react["default"].createElement(_Button["default"], (0, _extends2["default"])({}, tableButtonProps, button)));
}
});
if (buttonsLimit && buttons.length > buttonsLimit) {
var moreButton = getMoreButton(buttonsLimit);
children.push(moreButton);
}
}
return children;
}, [tableBtns, kanbanDS]);
var getCustomizationIcon = function getCustomizationIcon() {
var _customizedDS$current2;
// 是否存在切换面板按钮
var hasButtons = buttons && buttons.length; // 是否存在视图按钮控制
var shouldRenderTableBtns = !(0, _isFunction["default"])(renderButtons);
var tableButtons = getButtons();
var mergeButtons = buttons && buttons.length ? (0, _toConsumableArray2["default"])(buttons) : [];
if (hasButtons) {
var _customizedDS$current;
if (shouldRenderTableBtns) {
mergeButtons.push(tableButtons);
} else {
var getBtns = getButtons(renderButtons({
viewMode: _enum2.ViewMode.card,
dataSet: kanbanDS,
buttons: tableButtons
}));
mergeButtons.push(getBtns);
}
mergeButtons.unshift( /*#__PURE__*/_react["default"].createElement(_Button["default"], {
key: "settings",
className: "".concat(prefixCls, "-hover-button"),
funcType: _enum.FuncType.flat,
color: _enum.ButtonColor.primary,
icon: "settings-o",
hidden: ((_customizedDS$current = customizedDS.current) === null || _customizedDS$current === void 0 ? void 0 : _customizedDS$current.get(_enum2.ViewField.id)) === '__DEFAULT__',
onClick: openCustomizationModal
}));
return mergeButtons;
}
var buttonRenderer = [/*#__PURE__*/_react["default"].createElement(_Button["default"], {
key: "settings",
className: "".concat(prefixCls, "-hover-button"),
funcType: _enum.FuncType.flat,
color: _enum.ButtonColor.primary,
icon: "settings-o",
hidden: ((_customizedDS$current2 = customizedDS.current) === null || _customizedDS$current2 === void 0 ? void 0 : _customizedDS$current2.get(_enum2.ViewField.id)) === '__DEFAULT__',
onClick: openCustomizationModal
})];
if (shouldRenderTableBtns) {
buttonRenderer.unshift(tableBtns);
} else {
buttonRenderer.unshift(renderButtons({
viewMode: _enum2.ViewMode.card,
dataSet: kanbanDS,
buttons: tableBtns
}));
}
return buttonRenderer;
};
/**
* 渲染查询条字段组件
*/
var getQueryFields = (0, _react.useCallback)(function () {
var queryDataSet = kanbanDS.queryDataSet;
var result = [];
if (queryDataSet) {
var fields = queryDataSet.fields,
current = queryDataSet.current,
_queryDataSet$props$f = queryDataSet.props.fields,
propFields = _queryDataSet$props$f === void 0 ? [] : _queryDataSet$props$f;
var cloneFields = fields.toJS();
var tlsKey = getConfig('tlsKey');
var processField = function processField(field, name) {
if (!field.get('bind', current) && !name.includes(tlsKey)) {
var element = queryFields[name];
var filterBarProps = {};
var inValidElement = (0, _utils.getEditorByField)(field, current, true);
var placeholder = /*#__PURE__*/(0, _react.isValidElement)(element) && element.props.placeholder ? element.props.placeholder : (0, _utils.getPlaceholderByField)(field, current);
filterBarProps = {
placeholder: placeholder,
border: false,
clearButton: true
};
var elementType = inValidElement.type;
if ((! /*#__PURE__*/(0, _react.isValidElement)(element) || element.props.suffix === undefined) && ['Currency', 'ObserverNumberField', 'EmailField', 'UrlField', 'ObserverTextField'].indexOf(elementType.name) !== -1) {
(0, _extends2["default"])(filterBarProps, {
suffix: /*#__PURE__*/_react["default"].createElement(_icon["default"], {
type: "search"
})
});
}
var _props3 = (0, _objectSpread2["default"])({
key: name,
name: name,
dataSet: queryDataSet,
isFlat: true
}, filterBarProps);
result.push( /*#__PURE__*/(0, _react.isValidElement)(element) ? /*#__PURE__*/(0, _react.cloneElement)(element, _props3) : /*#__PURE__*/(0, _react.cloneElement)(inValidElement, (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, _props3), (0, _isObject["default"])(element) ? element : {})));
}
};
propFields.forEach(function (_ref4) {
var name = _ref4.name;
if (name) {
var field = cloneFields.get(name);
if (field) {
cloneFields["delete"](name);
processField(field, name);
}
}
});
cloneFields.forEach(function (field, name) {
processField(field, name);
});
}
return result;
}, []);
var restDragDropContext = (0, _react.useCallback)(function () {
var dragDropContext = kanbanProps.dragDropContext;
if ((0, _isFunction["default"])(dragDropContext)) {
return dragDropContext({
kanbanDS: kanbanDS,
dataSet: dataSet
});
}
return dragDropContext;
}, [kanbanDS, dataSet]);
return /*#__PURE__*/_react["default"].createElement("div", {
style: {
height: '100%'
}
}, kanbanDS && kanbanDS.queryDataSet ? /*#__PURE__*/_react["default"].createElement(_TableDynamicFilterBar["default"], (0, _extends2["default"])({}, queryBarProps, {
dataSet: kanbanDS,
queryDataSet: kanbanDS.queryDataSet,
queryFields: getQueryFields(),
buttons: getCustomizationIcon()
})) : getCustomizationIcon(), /*#__PURE__*/_react["default"].createElement(_reactBeautifulDnd.DragDropContext, (0, _extends2["default"])({}, restDragDropContext()), /*#__PURE__*/_react["default"].createElement(_grid.Row, {
gutter: 16,
className: cls
}, getBoardColumns())));
};
KanbanContent.defaultProps = {
animated: true
};
KanbanContent.displayName = 'KanbanContent';
var _default = (0, _mobxReactLite.observer)(KanbanContent);
exports["default"] = _default;
//# sourceMappingURL=KanbanContent.js.map