columnEditCollection.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. import {Component, createRef, Fragment} from 'react';
  2. import {createPortal} from 'react-dom';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {parseArithmetic} from 'sentry/components/arithmeticInput/parser';
  6. import {Button} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import {SectionHeading} from 'sentry/components/charts/styles';
  9. import Input from 'sentry/components/input';
  10. import {getOffsetOfElement} from 'sentry/components/performance/waterfall/utils';
  11. import {IconAdd, IconDelete, IconGrabbable} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {Organization} from 'sentry/types';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import {
  17. AGGREGATIONS,
  18. Column,
  19. generateFieldAsString,
  20. hasDuplicate,
  21. isLegalEquationColumn,
  22. } from 'sentry/utils/discover/fields';
  23. import theme from 'sentry/utils/theme';
  24. import {getPointerPosition} from 'sentry/utils/touch';
  25. import {setBodyUserSelect, UserSelectValues} from 'sentry/utils/userselect';
  26. import {WidgetType} from 'sentry/views/dashboards/types';
  27. import {FieldKey} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields';
  28. import {SESSIONS_OPERATIONS} from 'sentry/views/dashboards/widgetBuilder/releaseWidget/fields';
  29. import {generateFieldOptions} from '../utils';
  30. import {FieldValueOption, QueryField} from './queryField';
  31. import {FieldValueKind} from './types';
  32. type Sources = WidgetType;
  33. type Props = {
  34. // Input columns
  35. columns: Column[];
  36. fieldOptions: ReturnType<typeof generateFieldOptions>;
  37. // Fired when columns are added/removed/modified
  38. onChange: (columns: Column[]) => void;
  39. organization: Organization;
  40. className?: string;
  41. filterAggregateParameters?: (option: FieldValueOption) => boolean;
  42. filterPrimaryOptions?: (option: FieldValueOption) => boolean;
  43. noFieldsMessage?: string;
  44. showAliasField?: boolean;
  45. source?: Sources;
  46. };
  47. type State = {
  48. draggingGrabbedOffset: undefined | {x: number; y: number};
  49. draggingIndex: undefined | number;
  50. draggingTargetIndex: undefined | number;
  51. error: Map<number, string | undefined>;
  52. isDragging: boolean;
  53. left: undefined | number;
  54. top: undefined | number;
  55. };
  56. const DRAG_CLASS = 'draggable-item';
  57. const GHOST_PADDING = 4;
  58. const MAX_COL_COUNT = 20;
  59. enum PlaceholderPosition {
  60. TOP,
  61. BOTTOM,
  62. }
  63. class ColumnEditCollection extends Component<Props, State> {
  64. state: State = {
  65. isDragging: false,
  66. draggingIndex: void 0,
  67. draggingTargetIndex: void 0,
  68. draggingGrabbedOffset: void 0,
  69. error: new Map(),
  70. left: void 0,
  71. top: void 0,
  72. };
  73. componentDidMount() {
  74. if (!this.portal) {
  75. const portal = document.createElement('div');
  76. portal.style.position = 'absolute';
  77. portal.style.top = '0';
  78. portal.style.left = '0';
  79. portal.style.zIndex = String(theme.zIndex.modal);
  80. this.portal = portal;
  81. document.body.appendChild(this.portal);
  82. }
  83. this.checkColumnErrors(this.props.columns);
  84. }
  85. componentWillUnmount() {
  86. if (this.portal) {
  87. document.body.removeChild(this.portal);
  88. }
  89. this.cleanUpListeners();
  90. }
  91. checkColumnErrors(columns: Column[]) {
  92. const error = new Map();
  93. for (let i = 0; i < columns.length; i += 1) {
  94. const column = columns[i];
  95. if (column.kind === 'equation') {
  96. const result = parseArithmetic(column.field);
  97. if (result.error) {
  98. error.set(i, result.error);
  99. }
  100. }
  101. }
  102. this.setState({error});
  103. }
  104. previousUserSelect: UserSelectValues | null = null;
  105. portal: HTMLElement | null = null;
  106. dragGhostRef = createRef<HTMLDivElement>();
  107. keyForColumn(column: Column, isGhost: boolean): string {
  108. if (column.kind === 'function') {
  109. return [...column.function, isGhost].join(':');
  110. }
  111. return [...column.field, isGhost].join(':');
  112. }
  113. cleanUpListeners() {
  114. if (this.state.isDragging) {
  115. window.removeEventListener('mousemove', this.onDragMove);
  116. window.removeEventListener('touchmove', this.onDragMove);
  117. window.removeEventListener('mouseup', this.onDragEnd);
  118. window.removeEventListener('touchend', this.onDragEnd);
  119. }
  120. }
  121. // Signal to the parent that a new column has been added.
  122. handleAddColumn = () => {
  123. const newColumn: Column = {kind: 'field', field: ''};
  124. this.props.onChange([...this.props.columns, newColumn]);
  125. };
  126. handleAddEquation = () => {
  127. const {organization} = this.props;
  128. const newColumn: Column = {kind: FieldValueKind.EQUATION, field: ''};
  129. trackAnalytics('discover_v2.add_equation', {organization});
  130. this.props.onChange([...this.props.columns, newColumn]);
  131. };
  132. handleUpdateColumn = (index: number, updatedColumn: Column) => {
  133. const newColumns = [...this.props.columns];
  134. if (updatedColumn.kind === 'equation') {
  135. this.setState(prevState => {
  136. const error = new Map(prevState.error);
  137. error.set(index, parseArithmetic(updatedColumn.field).error);
  138. return {
  139. ...prevState,
  140. error,
  141. };
  142. });
  143. } else {
  144. // Update any equations that contain the existing column
  145. this.updateEquationFields(newColumns, index, updatedColumn);
  146. }
  147. newColumns.splice(index, 1, updatedColumn);
  148. this.props.onChange(newColumns);
  149. };
  150. updateEquationFields = (newColumns: Column[], index: number, updatedColumn: Column) => {
  151. const oldColumn = newColumns[index];
  152. const existingColumn = generateFieldAsString(newColumns[index]);
  153. const updatedColumnString = generateFieldAsString(updatedColumn);
  154. if (!isLegalEquationColumn(updatedColumn) || hasDuplicate(newColumns, oldColumn)) {
  155. return;
  156. }
  157. // Find the equations in the list of columns
  158. for (let i = 0; i < newColumns.length; i++) {
  159. const newColumn = newColumns[i];
  160. if (newColumn.kind === 'equation') {
  161. const result = parseArithmetic(newColumn.field);
  162. let newEquation = '';
  163. // Track where to continue from, not reconstructing from result so we don't have to worry
  164. // about spacing
  165. let lastIndex = 0;
  166. // the parser separates fields & functions, so we only need to check one
  167. const fields =
  168. oldColumn.kind === 'function' ? result.tc.functions : result.tc.fields;
  169. // for each field, add the text before it, then the new function and update index
  170. // to be where we want to start again
  171. for (const field of fields) {
  172. if (field.term === existingColumn && lastIndex !== field.location.end.offset) {
  173. newEquation +=
  174. newColumn.field.substring(lastIndex, field.location.start.offset) +
  175. updatedColumnString;
  176. lastIndex = field.location.end.offset;
  177. }
  178. }
  179. // Add whatever remains to be added from the equation, if existing field wasn't found
  180. // add the entire equation
  181. newEquation += newColumn.field.substring(lastIndex);
  182. newColumns[i] = {
  183. kind: 'equation',
  184. field: newEquation,
  185. alias: newColumns[i].alias,
  186. };
  187. }
  188. }
  189. };
  190. removeColumn(index: number) {
  191. const newColumns = [...this.props.columns];
  192. newColumns.splice(index, 1);
  193. this.checkColumnErrors(newColumns);
  194. this.props.onChange(newColumns);
  195. }
  196. startDrag(
  197. event: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>,
  198. index: number
  199. ) {
  200. const isDragging = this.state.isDragging;
  201. if (isDragging || !['mousedown', 'touchstart'].includes(event.type)) {
  202. return;
  203. }
  204. event.preventDefault();
  205. event.stopPropagation();
  206. const top = getPointerPosition(event, 'pageY');
  207. const left = getPointerPosition(event, 'pageX');
  208. // Compute where the user clicked on the drag handle. Avoids the element
  209. // jumping from the cursor on mousedown.
  210. const draggingElement = Array.from(document.querySelectorAll(`.${DRAG_CLASS}`)).find(
  211. n => n.contains(event.currentTarget)
  212. )!;
  213. const {x, y} = getOffsetOfElement(draggingElement);
  214. const draggingGrabbedOffset = {
  215. x: left - x + GHOST_PADDING,
  216. y: top - y + GHOST_PADDING,
  217. };
  218. // prevent the user from selecting things when dragging a column.
  219. this.previousUserSelect = setBodyUserSelect({
  220. userSelect: 'none',
  221. MozUserSelect: 'none',
  222. msUserSelect: 'none',
  223. webkitUserSelect: 'none',
  224. });
  225. // attach event listeners so that the mouse cursor can drag anywhere
  226. window.addEventListener('mousemove', this.onDragMove);
  227. window.addEventListener('touchmove', this.onDragMove);
  228. window.addEventListener('mouseup', this.onDragEnd);
  229. window.addEventListener('touchend', this.onDragEnd);
  230. this.setState({
  231. isDragging: true,
  232. draggingIndex: index,
  233. draggingTargetIndex: index,
  234. draggingGrabbedOffset,
  235. top,
  236. left,
  237. });
  238. }
  239. onDragMove = (event: MouseEvent | TouchEvent) => {
  240. const {isDragging, draggingTargetIndex, draggingGrabbedOffset} = this.state;
  241. if (!isDragging || !['mousemove', 'touchmove'].includes(event.type)) {
  242. return;
  243. }
  244. event.preventDefault();
  245. event.stopPropagation();
  246. const pointerX = getPointerPosition(event, 'pageX');
  247. const pointerY = getPointerPosition(event, 'pageY');
  248. const dragOffsetX = draggingGrabbedOffset?.x ?? 0;
  249. const dragOffsetY = draggingGrabbedOffset?.y ?? 0;
  250. if (this.dragGhostRef.current) {
  251. // move the ghost box
  252. const ghostDOM = this.dragGhostRef.current;
  253. // Adjust so cursor is over the grab handle.
  254. ghostDOM.style.left = `${pointerX - dragOffsetX}px`;
  255. ghostDOM.style.top = `${pointerY - dragOffsetY}px`;
  256. }
  257. const dragItems = document.querySelectorAll(`.${DRAG_CLASS}`);
  258. // Find the item that the ghost is currently over.
  259. const targetIndex = Array.from(dragItems).findIndex(dragItem => {
  260. const rects = dragItem.getBoundingClientRect();
  261. const top = pointerY;
  262. const thresholdStart = window.scrollY + rects.top;
  263. const thresholdEnd = window.scrollY + rects.top + rects.height;
  264. return top >= thresholdStart && top <= thresholdEnd;
  265. });
  266. // Issue column in Issue widgets are fixed (cannot be moved or deleted)
  267. if (targetIndex >= 0 && targetIndex !== draggingTargetIndex) {
  268. this.setState({draggingTargetIndex: targetIndex});
  269. }
  270. };
  271. isFixedIssueColumn = (columnIndex: number) => {
  272. const {source, columns} = this.props;
  273. const column = columns[columnIndex];
  274. const issueFieldColumnCount = columns.filter(
  275. col => col.kind === 'field' && col.field === FieldKey.ISSUE
  276. ).length;
  277. return (
  278. issueFieldColumnCount <= 1 &&
  279. source === WidgetType.ISSUE &&
  280. column.kind === 'field' &&
  281. column.field === FieldKey.ISSUE
  282. );
  283. };
  284. isRemainingReleaseHealthAggregate = (columnIndex: number) => {
  285. const {source, columns} = this.props;
  286. const column = columns[columnIndex];
  287. const aggregateCount = columns.filter(
  288. col => col.kind === FieldValueKind.FUNCTION
  289. ).length;
  290. return (
  291. aggregateCount <= 1 &&
  292. source === WidgetType.RELEASE &&
  293. column.kind === FieldValueKind.FUNCTION
  294. );
  295. };
  296. onDragEnd = (event: MouseEvent | TouchEvent) => {
  297. if (!this.state.isDragging || !['mouseup', 'touchend'].includes(event.type)) {
  298. return;
  299. }
  300. const sourceIndex = this.state.draggingIndex;
  301. const targetIndex = this.state.draggingTargetIndex;
  302. if (typeof sourceIndex !== 'number' || typeof targetIndex !== 'number') {
  303. return;
  304. }
  305. // remove listeners that were attached in startColumnDrag
  306. this.cleanUpListeners();
  307. // restore body user-select values
  308. if (this.previousUserSelect) {
  309. setBodyUserSelect(this.previousUserSelect);
  310. this.previousUserSelect = null;
  311. }
  312. // Reorder columns and trigger change.
  313. const newColumns = [...this.props.columns];
  314. const removed = newColumns.splice(sourceIndex, 1);
  315. newColumns.splice(targetIndex, 0, removed[0]);
  316. this.checkColumnErrors(newColumns);
  317. this.props.onChange(newColumns);
  318. this.setState({
  319. isDragging: false,
  320. left: undefined,
  321. top: undefined,
  322. draggingIndex: undefined,
  323. draggingTargetIndex: undefined,
  324. draggingGrabbedOffset: undefined,
  325. });
  326. };
  327. renderGhost({gridColumns, singleColumn}: {gridColumns: number; singleColumn: boolean}) {
  328. const {isDragging, draggingIndex, draggingGrabbedOffset} = this.state;
  329. const index = draggingIndex;
  330. if (typeof index !== 'number' || !isDragging || !this.portal) {
  331. return null;
  332. }
  333. const dragOffsetX = draggingGrabbedOffset?.x ?? 0;
  334. const dragOffsetY = draggingGrabbedOffset?.y ?? 0;
  335. const top = Number(this.state.top) - dragOffsetY;
  336. const left = Number(this.state.left) - dragOffsetX;
  337. const col = this.props.columns[index];
  338. const style = {
  339. top: `${top}px`,
  340. left: `${left}px`,
  341. };
  342. const ghost = (
  343. <Ghost ref={this.dragGhostRef} style={style}>
  344. {this.renderItem(col, index, {
  345. singleColumn,
  346. isGhost: true,
  347. gridColumns,
  348. })}
  349. </Ghost>
  350. );
  351. return createPortal(ghost, this.portal);
  352. }
  353. renderItem(
  354. col: Column,
  355. i: number,
  356. {
  357. singleColumn = false,
  358. canDelete = true,
  359. canDrag = true,
  360. isGhost = false,
  361. gridColumns = 2,
  362. disabled = false,
  363. }: {
  364. gridColumns: number;
  365. singleColumn: boolean;
  366. canDelete?: boolean;
  367. canDrag?: boolean;
  368. disabled?: boolean;
  369. isGhost?: boolean;
  370. }
  371. ) {
  372. const {
  373. columns,
  374. fieldOptions,
  375. filterAggregateParameters,
  376. filterPrimaryOptions,
  377. noFieldsMessage,
  378. showAliasField,
  379. } = this.props;
  380. const {isDragging, draggingTargetIndex, draggingIndex} = this.state;
  381. let placeholder: React.ReactNode = null;
  382. // Add a placeholder above the target row.
  383. if (isDragging && isGhost === false && draggingTargetIndex === i) {
  384. placeholder = (
  385. <DragPlaceholder
  386. key={`placeholder:${this.keyForColumn(col, isGhost)}`}
  387. className={DRAG_CLASS}
  388. />
  389. );
  390. }
  391. // If the current row is the row in the drag ghost return the placeholder
  392. // or a hole if the placeholder is elsewhere.
  393. if (isDragging && isGhost === false && draggingIndex === i) {
  394. return placeholder;
  395. }
  396. const position =
  397. Number(draggingTargetIndex) <= Number(draggingIndex)
  398. ? PlaceholderPosition.TOP
  399. : PlaceholderPosition.BOTTOM;
  400. return (
  401. <Fragment key={`${i}:${this.keyForColumn(col, isGhost)}`}>
  402. {position === PlaceholderPosition.TOP && placeholder}
  403. <RowContainer
  404. showAliasField={showAliasField}
  405. singleColumn={singleColumn}
  406. className={isGhost ? '' : DRAG_CLASS}
  407. >
  408. {canDrag ? (
  409. <DragAndReorderButton
  410. aria-label={t('Drag to reorder')}
  411. onMouseDown={event => this.startDrag(event, i)}
  412. onTouchStart={event => this.startDrag(event, i)}
  413. icon={<IconGrabbable size="xs" />}
  414. size="zero"
  415. borderless
  416. />
  417. ) : singleColumn && showAliasField ? null : (
  418. <span />
  419. )}
  420. <QueryField
  421. fieldOptions={fieldOptions}
  422. gridColumns={gridColumns}
  423. fieldValue={col}
  424. onChange={value => this.handleUpdateColumn(i, value)}
  425. error={this.state.error.get(i)}
  426. takeFocus={i === this.props.columns.length - 1}
  427. otherColumns={columns}
  428. shouldRenderTag
  429. disabled={disabled}
  430. filterPrimaryOptions={filterPrimaryOptions}
  431. filterAggregateParameters={filterAggregateParameters}
  432. noFieldsMessage={noFieldsMessage}
  433. skipParameterPlaceholder={showAliasField}
  434. />
  435. {showAliasField && (
  436. <AliasField singleColumn={singleColumn}>
  437. <AliasInput
  438. name="alias"
  439. placeholder={t('Alias')}
  440. value={col.alias ?? ''}
  441. onChange={value => {
  442. this.handleUpdateColumn(i, {
  443. ...col,
  444. alias: value.target.value,
  445. });
  446. }}
  447. />
  448. </AliasField>
  449. )}
  450. {canDelete || col.kind === 'equation' ? (
  451. showAliasField ? (
  452. <RemoveButton
  453. data-test-id={`remove-column-${i}`}
  454. aria-label={t('Remove column')}
  455. title={t('Remove column')}
  456. onClick={() => this.removeColumn(i)}
  457. icon={<IconDelete />}
  458. borderless
  459. />
  460. ) : (
  461. <RemoveButton
  462. data-test-id={`remove-column-${i}`}
  463. aria-label={t('Remove column')}
  464. onClick={() => this.removeColumn(i)}
  465. icon={<IconDelete />}
  466. borderless
  467. />
  468. )
  469. ) : singleColumn && showAliasField ? null : (
  470. <span />
  471. )}
  472. </RowContainer>
  473. {position === PlaceholderPosition.BOTTOM && placeholder}
  474. </Fragment>
  475. );
  476. }
  477. render() {
  478. const {className, columns, showAliasField, source} = this.props;
  479. const canDelete = columns.filter(field => field.kind !== 'equation').length > 1;
  480. const canDrag = columns.length > 1;
  481. const canAdd = columns.length < MAX_COL_COUNT;
  482. const title = canAdd
  483. ? undefined
  484. : t(
  485. `Sorry, you've reached the maximum number of columns (%d). Delete columns to add more.`,
  486. MAX_COL_COUNT
  487. );
  488. const singleColumn = columns.length === 1;
  489. // Get the longest number of columns so we can layout the rows.
  490. // We always want at least 2 columns.
  491. const gridColumns =
  492. source === WidgetType.ISSUE
  493. ? 1
  494. : Math.max(
  495. ...columns.map(col => {
  496. if (col.kind !== 'function') {
  497. return 2;
  498. }
  499. const operation =
  500. AGGREGATIONS[col.function[0]] ?? SESSIONS_OPERATIONS[col.function[0]];
  501. if (!operation || !operation.parameters) {
  502. // Operation should be in the look-up table, but not all operations are (eg. private). This should be changed at some point.
  503. return 3;
  504. }
  505. return operation.parameters.length === 2 ? 3 : 2;
  506. })
  507. );
  508. // TODO(ddm): support multiple columns and equations, then remove this check
  509. const showActionButtons = source !== WidgetType.METRICS;
  510. return (
  511. <div className={className}>
  512. {this.renderGhost({gridColumns, singleColumn})}
  513. {!showAliasField && source !== WidgetType.ISSUE && (
  514. <RowContainer showAliasField={showAliasField} singleColumn={singleColumn}>
  515. <Heading gridColumns={gridColumns}>
  516. <StyledSectionHeading>{t('Tag / Field / Function')}</StyledSectionHeading>
  517. <StyledSectionHeading>{t('Field Parameter')}</StyledSectionHeading>
  518. </Heading>
  519. </RowContainer>
  520. )}
  521. {columns.map((col: Column, i: number) => {
  522. // Issue column in Issue widgets are fixed (cannot be changed or deleted)
  523. if (this.isFixedIssueColumn(i)) {
  524. return this.renderItem(col, i, {
  525. singleColumn,
  526. canDelete: false,
  527. canDrag,
  528. gridColumns,
  529. disabled: true,
  530. });
  531. }
  532. if (this.isRemainingReleaseHealthAggregate(i)) {
  533. return this.renderItem(col, i, {
  534. singleColumn,
  535. canDelete: false,
  536. canDrag,
  537. gridColumns,
  538. });
  539. }
  540. return this.renderItem(col, i, {
  541. singleColumn,
  542. canDelete,
  543. canDrag,
  544. gridColumns,
  545. });
  546. })}
  547. {showActionButtons && (
  548. <RowContainer showAliasField={showAliasField} singleColumn={singleColumn}>
  549. <Actions gap={1} showAliasField={showAliasField}>
  550. <Button
  551. size="sm"
  552. aria-label={t('Add a Column')}
  553. onClick={this.handleAddColumn}
  554. title={title}
  555. disabled={!canAdd}
  556. icon={<IconAdd isCircled size="xs" />}
  557. >
  558. {t('Add a Column')}
  559. </Button>
  560. {source !== WidgetType.ISSUE && source !== WidgetType.RELEASE && (
  561. <Button
  562. size="sm"
  563. aria-label={t('Add an Equation')}
  564. onClick={this.handleAddEquation}
  565. title={title}
  566. disabled={!canAdd}
  567. icon={<IconAdd isCircled size="xs" />}
  568. >
  569. {t('Add an Equation')}
  570. </Button>
  571. )}
  572. </Actions>
  573. </RowContainer>
  574. )}
  575. </div>
  576. );
  577. }
  578. }
  579. const Actions = styled(ButtonBar)<{showAliasField?: boolean}>`
  580. grid-column: ${p => (p.showAliasField ? '1/-1' : ' 2/3')};
  581. justify-content: flex-start;
  582. `;
  583. const RowContainer = styled('div')<{
  584. singleColumn: boolean;
  585. showAliasField?: boolean;
  586. }>`
  587. display: grid;
  588. grid-template-columns: ${space(3)} 1fr 40px;
  589. justify-content: center;
  590. align-items: center;
  591. width: 100%;
  592. touch-action: none;
  593. padding-bottom: ${space(1)};
  594. ${p =>
  595. p.showAliasField &&
  596. css`
  597. align-items: flex-start;
  598. grid-template-columns: ${p.singleColumn ? `1fr` : `${space(3)} 1fr 40px`};
  599. @media (min-width: ${p.theme.breakpoints.small}) {
  600. grid-template-columns: ${p.singleColumn
  601. ? `1fr calc(200px + ${space(1)})`
  602. : `${space(3)} 1fr calc(200px + ${space(1)}) 40px`};
  603. }
  604. `};
  605. `;
  606. const Ghost = styled('div')`
  607. background: ${p => p.theme.background};
  608. display: block;
  609. position: absolute;
  610. padding: ${GHOST_PADDING}px;
  611. border-radius: ${p => p.theme.borderRadius};
  612. box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
  613. width: 710px;
  614. opacity: 0.8;
  615. cursor: grabbing;
  616. padding-right: ${space(2)};
  617. & > ${RowContainer} {
  618. padding-bottom: 0;
  619. }
  620. & svg {
  621. cursor: grabbing;
  622. }
  623. `;
  624. const DragPlaceholder = styled('div')`
  625. margin: 0 ${space(3)} ${space(1)} ${space(3)};
  626. border: 2px dashed ${p => p.theme.border};
  627. border-radius: ${p => p.theme.borderRadius};
  628. height: ${p => p.theme.form.md.height}px;
  629. `;
  630. const Heading = styled('div')<{gridColumns: number}>`
  631. grid-column: 2 / 3;
  632. /* Emulate the grid used in the column editor rows */
  633. display: grid;
  634. grid-template-columns: repeat(${p => p.gridColumns}, 1fr);
  635. grid-column-gap: ${space(1)};
  636. `;
  637. const StyledSectionHeading = styled(SectionHeading)`
  638. margin: 0;
  639. `;
  640. const AliasInput = styled(Input)`
  641. min-width: 50px;
  642. `;
  643. const AliasField = styled('div')<{singleColumn: boolean}>`
  644. margin-top: ${space(1)};
  645. @media (min-width: ${p => p.theme.breakpoints.small}) {
  646. margin-top: 0;
  647. margin-left: ${space(1)};
  648. }
  649. @media (max-width: ${p => p.theme.breakpoints.small}) {
  650. grid-row: 2/2;
  651. grid-column: ${p => (p.singleColumn ? '1/-1' : '2/2')};
  652. }
  653. `;
  654. const RemoveButton = styled(Button)`
  655. margin-left: ${space(1)};
  656. height: ${p => p.theme.form.md.height}px;
  657. `;
  658. const DragAndReorderButton = styled(Button)`
  659. height: ${p => p.theme.form.md.height}px;
  660. `;
  661. export default ColumnEditCollection;