From 6426f366da599dca80f0165da1e7dc040f9d2d4c Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Mon, 11 Apr 2022 13:57:47 +0300
Subject: [PATCH] Move events between timelines (#21557)

---
 frontend/src/metabase-types/api/timeline.ts   |   2 +
 .../src/metabase/entities/timeline-events.js  |  15 ++-
 frontend/src/metabase/entities/timelines.js   |  33 +++++-
 frontend/src/metabase/lib/timelines.ts        |  17 +++
 frontend/src/metabase/lib/urls.js             |  33 +++---
 .../query_builder/components/QueryModals.jsx  |   9 ++
 .../TimelineSidebar/TimelineSidebar.tsx       |   8 ++
 .../src/metabase/query_builder/constants.js   |   1 +
 .../src/metabase/query_builder/selectors.js   |  12 +--
 .../DeleteEventModal.styled.tsx               |  11 --
 .../DeleteEventModal/DeleteEventModal.tsx     |  46 --------
 .../DeleteTimelineModal.styled.tsx            |  11 --
 .../DeleteTimelineModal.tsx                   |  40 -------
 .../EditEventModal/EditEventModal.styled.tsx  |  18 ----
 .../EditTimelineModal.styled.tsx              |  18 ----
 .../components/EventCard/EventCard.tsx        |  23 ++--
 .../EventCard/EventCard.unit.spec.tsx         |   1 -
 .../components/EventList/EventList.tsx        |   5 +-
 .../EventList/EventList.unit.spec.tsx         |   2 -
 .../NewEventModal/NewEventModal.tsx           |  66 ------------
 .../NewEventModal/NewEventModal.unit.spec.tsx |  32 ------
 .../NewTimelineModal.styled.tsx               |   5 -
 .../components/TimelineCard/TimelineCard.tsx  |  23 ++--
 .../TimelineCard/TimelineCard.unit.spec.tsx   |   2 -
 .../TimelineDetailsModal.tsx                  |  28 +++--
 .../TimelineDetailsModal.unit.spec.tsx        |   1 -
 .../TimelineEmptyState/TimelineEmptyState.tsx |   6 +-
 .../components/TimelineList/TimelineList.tsx  |   5 +-
 .../TimelineListModal/TimelineListModal.tsx   |  23 ++--
 .../DeleteEventModal/DeleteEventModal.tsx     |  29 ++---
 .../DeleteTimelineModal.tsx                   |  23 ++--
 .../EditEventModal/EditEventModal.tsx         |  39 +++----
 .../EditTimelineModal/EditTimelineModal.tsx   |  28 +++--
 .../MoveEventModal/MoveEventModal.tsx         |  55 ++++++++++
 .../containers/MoveEventModal/index.ts        |   1 +
 .../NewEventModal/NewEventModal.tsx           |  26 ++---
 .../NewEventWithTimelineModal.tsx             |  10 +-
 .../NewTimelineModal/NewTimelineModal.tsx     |  16 +--
 .../TimelineArchiveModal.tsx                  |  22 ++--
 .../TimelineDetailsModal.tsx                  |  17 +--
 .../TimelineIndexModal/TimelineIndexModal.tsx |  10 +-
 .../TimelineListArchiveModal.tsx              |  13 ++-
 .../TimelineListModal/TimelineListModal.tsx   |  13 ++-
 .../metabase/timelines/collections/routes.tsx |   8 ++
 .../metabase/timelines/collections/types.ts   |   4 -
 .../DeleteEventModal/DeleteEventModal.tsx     |  41 +++++++
 .../DeleteEventModal.unit.spec.tsx            |   2 -
 .../components/DeleteEventModal/index.ts      |   0
 .../DeleteTimelineModal.tsx                   |  39 +++++++
 .../components/DeleteTimelineModal/index.ts   |   0
 .../EditEventModal/EditEventModal.tsx         |  41 ++++---
 .../EditEventModal.unit.spec.tsx              |   2 -
 .../components/EditEventModal/index.ts        |   0
 .../EditTimelineModal/EditTimelineModal.tsx   |  31 +++---
 .../EditTimelineModal.unit.spec.tsx           |   6 +-
 .../components/EditTimelineModal/index.ts     |   0
 .../ModalBody/ModalBody.styled.tsx}           |   2 +-
 .../common/components/ModalBody/ModalBody.tsx |  12 +++
 .../common/components/ModalBody/index.ts      |   1 +
 .../ModalDangerButton.styled.tsx}             |   6 +-
 .../ModalDangerButton/ModalDangerButton.tsx   |  20 ++++
 .../components/ModalDangerButton/index.ts     |   1 +
 .../ModalFooter/ModalFooter.styled.tsx        |  12 +++
 .../components/ModalFooter/ModalFooter.tsx    |  16 +++
 .../common/components/ModalFooter/index.ts    |   1 +
 .../MoveEventModal/MoveEventModal.styled.tsx  |  21 ++++
 .../MoveEventModal/MoveEventModal.tsx         |  67 ++++++++++++
 .../MoveEventModal.unit.spec.tsx              |  43 ++++++++
 .../common/components/MoveEventModal/index.ts |   1 +
 .../NewEventModal/NewEventModal.tsx           |  42 +++++---
 .../components/NewEventModal/index.ts         |   0
 .../NewTimelineModal/NewTimelineModal.tsx     |  11 +-
 .../NewTimelineModal.unit.spec.tsx            |   0
 .../components/NewTimelineModal/index.ts      |   0
 .../TimelinePicker/TimelinePicker.stories.tsx |  45 ++++++++
 .../TimelinePicker/TimelinePicker.styled.tsx  |  89 +++++++++++++++
 .../TimelinePicker/TimelinePicker.tsx         |  74 +++++++++++++
 .../common/components/TimelinePicker/index.ts |   1 +
 .../EditEventModal/EditEventModal.tsx         |  58 ----------
 .../EditEventModal.unit.spec.tsx              |  34 ------
 .../components/EditEventModal/index.ts        |   1 -
 .../components/EventCard/EventCard.tsx        |   9 +-
 .../NewEventModal/NewEventModal.styled.tsx    |   5 -
 .../NewEventModal/NewEventModal.unit.spec.tsx |  31 ------
 .../components/NewEventModal/index.ts         |   1 -
 .../components/TimelineCard/TimelineCard.tsx  |   3 +
 .../components/TimelineList/TimelineList.tsx  |   3 +
 .../TimelinePanel/TimelinePanel.tsx           |   3 +
 .../EditEventModal/EditEventModal.tsx         |  17 ++-
 .../MoveEventModal/MoveEventModal.tsx         |  53 +++++++++
 .../containers/MoveEventModal/index.ts        |   1 +
 .../NewEventModal/NewEventModal.tsx           |  15 ++-
 .../collections/timelines.cy.spec.js          |   4 +-
 .../collections/timelines.cy.spec.js          |  49 +++++++++
 .../scenarios/question/timelines.cy.spec.js   | 101 +++++++++++-------
 95 files changed, 1094 insertions(+), 731 deletions(-)
 delete mode 100644 frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.styled.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.styled.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.styled.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.styled.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.unit.spec.tsx
 delete mode 100644 frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.styled.tsx
 create mode 100644 frontend/src/metabase/timelines/collections/containers/MoveEventModal/MoveEventModal.tsx
 create mode 100644 frontend/src/metabase/timelines/collections/containers/MoveEventModal/index.ts
 create mode 100644 frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.tsx
 rename frontend/src/metabase/timelines/{collections => common}/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx (92%)
 rename frontend/src/metabase/timelines/{collections => common}/components/DeleteEventModal/index.ts (100%)
 create mode 100644 frontend/src/metabase/timelines/common/components/DeleteTimelineModal/DeleteTimelineModal.tsx
 rename frontend/src/metabase/timelines/{collections => common}/components/DeleteTimelineModal/index.ts (100%)
 rename frontend/src/metabase/timelines/{collections => common}/components/EditEventModal/EditEventModal.tsx (57%)
 rename frontend/src/metabase/timelines/{collections => common}/components/EditEventModal/EditEventModal.unit.spec.tsx (94%)
 rename frontend/src/metabase/timelines/{collections => common}/components/EditEventModal/index.ts (100%)
 rename frontend/src/metabase/timelines/{collections => common}/components/EditTimelineModal/EditTimelineModal.tsx (63%)
 rename frontend/src/metabase/timelines/{collections => common}/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx (87%)
 rename frontend/src/metabase/timelines/{collections => common}/components/EditTimelineModal/index.ts (100%)
 rename frontend/src/metabase/timelines/{collections/components/NewEventModal/NewEventModal.styled.tsx => common/components/ModalBody/ModalBody.styled.tsx} (61%)
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalBody/index.ts
 rename frontend/src/metabase/timelines/{questions/components/EditEventModal/EditEventModal.styled.tsx => common/components/ModalDangerButton/ModalDangerButton.styled.tsx} (73%)
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalDangerButton/index.ts
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.styled.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/ModalFooter/index.ts
 create mode 100644 frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.styled.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.unit.spec.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/MoveEventModal/index.ts
 rename frontend/src/metabase/timelines/{questions => common}/components/NewEventModal/NewEventModal.tsx (68%)
 rename frontend/src/metabase/timelines/{collections => common}/components/NewEventModal/index.ts (100%)
 rename frontend/src/metabase/timelines/{collections => common}/components/NewTimelineModal/NewTimelineModal.tsx (85%)
 rename frontend/src/metabase/timelines/{collections => common}/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx (100%)
 rename frontend/src/metabase/timelines/{collections => common}/components/NewTimelineModal/index.ts (100%)
 create mode 100644 frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.stories.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.styled.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.tsx
 create mode 100644 frontend/src/metabase/timelines/common/components/TimelinePicker/index.ts
 delete mode 100644 frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.tsx
 delete mode 100644 frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.unit.spec.tsx
 delete mode 100644 frontend/src/metabase/timelines/questions/components/EditEventModal/index.ts
 delete mode 100644 frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.styled.tsx
 delete mode 100644 frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.unit.spec.tsx
 delete mode 100644 frontend/src/metabase/timelines/questions/components/NewEventModal/index.ts
 create mode 100644 frontend/src/metabase/timelines/questions/containers/MoveEventModal/MoveEventModal.tsx
 create mode 100644 frontend/src/metabase/timelines/questions/containers/MoveEventModal/index.ts

diff --git a/frontend/src/metabase-types/api/timeline.ts b/frontend/src/metabase-types/api/timeline.ts
index 2aa4437f669..c2db3554b0d 100644
--- a/frontend/src/metabase-types/api/timeline.ts
+++ b/frontend/src/metabase-types/api/timeline.ts
@@ -26,3 +26,5 @@ export interface TimelineEvent {
   creator: User;
   created_at: string;
 }
+
+export type TimelineEventSource = "question" | "collections" | "api";
diff --git a/frontend/src/metabase/entities/timeline-events.js b/frontend/src/metabase/entities/timeline-events.js
index 8e31950a377..547e49f91ea 100644
--- a/frontend/src/metabase/entities/timeline-events.js
+++ b/frontend/src/metabase/entities/timeline-events.js
@@ -11,12 +11,21 @@ const TimelineEvents = createEntity({
   forms,
 
   objectActions: {
-    setArchived: ({ id }, archived, opts) =>
-      TimelineEvents.actions.update(
+    setTimeline: ({ id }, timeline, opts) => {
+      return TimelineEvents.actions.update(
+        { id },
+        { timeline_id: timeline.id },
+        undo(opts, t`event`, t`moved`),
+      );
+    },
+
+    setArchived: ({ id }, archived, opts) => {
+      return TimelineEvents.actions.update(
         { id },
         { archived },
         undo(opts, t`event`, archived ? t`archived` : t`unarchived`),
-      ),
+      );
+    },
   },
 });
 
diff --git a/frontend/src/metabase/entities/timelines.js b/frontend/src/metabase/entities/timelines.js
index 87aa80221be..4ef0f84e9ed 100644
--- a/frontend/src/metabase/entities/timelines.js
+++ b/frontend/src/metabase/entities/timelines.js
@@ -45,16 +45,39 @@ const Timelines = createEntity({
   reducer: (state = {}, action) => {
     if (action.type === TimelineEvents.actionTypes.CREATE) {
       const event = TimelineEvents.HACK_getObjectFromAction(action);
-      return updateIn(state, [event.timeline_id, "events"], (events = []) => {
-        return [...events, event.id];
+
+      return updateIn(state, [event.timeline_id, "events"], (eventIds = []) => {
+        return [...eventIds, event.id];
+      });
+    }
+
+    if (action.type === TimelineEvents.actionTypes.UPDATE) {
+      const event = TimelineEvents.HACK_getObjectFromAction(action);
+
+      return _.mapObject(state, timeline => {
+        const hasEvent = timeline.events?.includes(event.id);
+        const hasTimeline = event.timeline_id === timeline.id;
+
+        return updateIn(timeline, ["events"], (eventIds = []) => {
+          if (hasEvent && !hasTimeline) {
+            return _.without(eventIds, event.id);
+          } else if (!hasEvent && hasTimeline) {
+            return [...eventIds, event.id];
+          } else {
+            return eventIds;
+          }
+        });
       });
     }
 
     if (action.type === TimelineEvents.actionTypes.DELETE) {
       const eventId = action.payload.result;
-      return _.mapObject(state, timeline =>
-        updateIn(timeline, ["events"], events => _.without(events, eventId)),
-      );
+
+      return _.mapObject(state, timeline => {
+        return updateIn(timeline, ["events"], (eventIds = []) => {
+          return _.without(eventIds, eventId);
+        });
+      });
     }
 
     return state;
diff --git a/frontend/src/metabase/lib/timelines.ts b/frontend/src/metabase/lib/timelines.ts
index 81207c91431..051881830c5 100644
--- a/frontend/src/metabase/lib/timelines.ts
+++ b/frontend/src/metabase/lib/timelines.ts
@@ -1,3 +1,4 @@
+import _ from "underscore";
 import { t } from "ttag";
 import { Collection, Timeline } from "metabase-types/api";
 import { canonicalCollectionId } from "metabase/collections/utils";
@@ -37,3 +38,19 @@ export const getDefaultTimelineName = (collection: Collection) => {
 export const getDefaultTimelineIcon = () => {
   return "star";
 };
+
+export const getSortedTimelines = (
+  timelines: Timeline[],
+  collection?: Collection,
+) => {
+  return _.chain(timelines)
+    .sortBy(timeline => getTimelineName(timeline).toLowerCase())
+    .sortBy(timeline => timeline.collection?.personal_owner_id != null) // personal collections last
+    .sortBy(timeline => !timeline.default) // default timelines first
+    .sortBy(timeline => timeline.collection?.id !== collection?.id) // timelines within the collection first
+    .value();
+};
+
+export const getEventCount = ({ events = [], archived }: Timeline) => {
+  return events.filter(e => e.archived === archived).length;
+};
diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js
index 7f1ff02e206..a044c40ad30 100644
--- a/frontend/src/metabase/lib/urls.js
+++ b/frontend/src/metabase/lib/urls.js
@@ -305,41 +305,46 @@ export function timelinesArchiveInCollection(collection) {
   return `${timelinesInCollection(collection)}/archive`;
 }
 
-export function timelineInCollection(timeline, collection) {
-  return `${timelinesInCollection(collection)}/${timeline.id}`;
+export function timelineInCollection(timeline) {
+  return `${timelinesInCollection(timeline.collection)}/${timeline.id}`;
 }
 
 export function newTimelineInCollection(collection) {
   return `${timelinesInCollection(collection)}/new`;
 }
 
-export function editTimelineInCollection(timeline, collection) {
-  return `${timelineInCollection(timeline, collection)}/edit`;
+export function editTimelineInCollection(timeline) {
+  return `${timelineInCollection(timeline)}/edit`;
 }
 
-export function timelineArchiveInCollection(timeline, collection) {
-  return `${timelineInCollection(timeline, collection)}/archive`;
+export function timelineArchiveInCollection(timeline) {
+  return `${timelineInCollection(timeline)}/archive`;
 }
 
-export function deleteTimelineInCollection(timeline, collection) {
-  return `${timelineInCollection(timeline, collection)}/delete`;
+export function deleteTimelineInCollection(timeline) {
+  return `${timelineInCollection(timeline)}/delete`;
 }
 
-export function newEventInCollection(timeline, collection) {
-  return `${timelineInCollection(timeline, collection)}/events/new`;
+export function newEventInCollection(timeline) {
+  return `${timelineInCollection(timeline)}/events/new`;
 }
 
 export function newEventAndTimelineInCollection(collection) {
   return `${timelinesInCollection(collection)}/new/events/new`;
 }
 
-export function editEventInCollection(event, timeline, collection) {
-  const timelineUrl = timelineInCollection(timeline, collection);
+export function editEventInCollection(event, timeline) {
+  const timelineUrl = timelineInCollection(timeline);
   return `${timelineUrl}/events/${event.id}/edit`;
 }
 
-export function deleteEventInCollection(event, timeline, collection) {
-  const timelineUrl = timelineInCollection(timeline, collection);
+export function moveEventInCollection(event, timeline) {
+  const timelineUrl = timelineInCollection(timeline);
+  return `${timelineUrl}/events/${event.id}/move`;
+}
+
+export function deleteEventInCollection(event, timeline) {
+  const timelineUrl = timelineInCollection(timeline);
   return `${timelineUrl}/events/${event.id}/delete`;
 }
 
diff --git a/frontend/src/metabase/query_builder/components/QueryModals.jsx b/frontend/src/metabase/query_builder/components/QueryModals.jsx
index 370300ac33e..aef2a9da4ca 100644
--- a/frontend/src/metabase/query_builder/components/QueryModals.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModals.jsx
@@ -24,6 +24,7 @@ import NewDatasetModal from "metabase/query_builder/components/NewDatasetModal";
 import EntityCopyModal from "metabase/entities/containers/EntityCopyModal";
 import NewEventModal from "metabase/timelines/questions/containers/NewEventModal";
 import EditEventModal from "metabase/timelines/questions/containers/EditEventModal";
+import MoveEventModal from "metabase/timelines/questions/containers/MoveEventModal";
 
 export default class QueryModals extends React.Component {
   showAlertsAfterQuestionSaved = () => {
@@ -233,6 +234,14 @@ export default class QueryModals extends React.Component {
       <Modal onClose={onCloseModal}>
         <EditEventModal eventId={modalContext} onClose={onCloseModal} />
       </Modal>
+    ) : modal === MODAL_TYPES.MOVE_EVENT ? (
+      <Modal onClose={onCloseModal}>
+        <MoveEventModal
+          eventId={modalContext}
+          collectionId={question.collectionId()}
+          onClose={onCloseModal}
+        />
+      </Modal>
     ) : null;
   }
 }
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx
index eed7c7ddd80..f21a85842e1 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/TimelineSidebar/TimelineSidebar.tsx
@@ -45,6 +45,13 @@ const TimelineSidebar = ({
     [onOpenModal],
   );
 
+  const handleMoveEvent = useCallback(
+    (event: TimelineEvent) => {
+      onOpenModal?.(MODAL_TYPES.MOVE_EVENT, event.id);
+    },
+    [onOpenModal],
+  );
+
   const handleToggleEvent = useCallback(
     (event: TimelineEvent, isSelected: boolean) => {
       if (isSelected) {
@@ -76,6 +83,7 @@ const TimelineSidebar = ({
         selectedEventIds={selectedTimelineEventIds}
         onNewEvent={handleNewEvent}
         onEditEvent={handleEditEvent}
+        onMoveEvent={handleMoveEvent}
         onToggleEvent={handleToggleEvent}
         onToggleTimeline={handleToggleTimeline}
       />
diff --git a/frontend/src/metabase/query_builder/constants.js b/frontend/src/metabase/query_builder/constants.js
index 7ab1ee5c935..83955956270 100644
--- a/frontend/src/metabase/query_builder/constants.js
+++ b/frontend/src/metabase/query_builder/constants.js
@@ -16,6 +16,7 @@ export const MODAL_TYPES = {
   CAN_NOT_CREATE_MODEL: "can-not-create-model",
   NEW_EVENT: "new-event",
   EDIT_EVENT: "edit-event",
+  MOVE_EVENT: "move-event",
 };
 
 export const SIDEBAR_SIZES = {
diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js
index 2d27480f233..7a98f1ad6ec 100644
--- a/frontend/src/metabase/query_builder/selectors.js
+++ b/frontend/src/metabase/query_builder/selectors.js
@@ -27,7 +27,7 @@ import Timelines from "metabase/entities/timelines";
 import { getMetadata } from "metabase/selectors/metadata";
 import { getAlerts } from "metabase/alert/selectors";
 import { parseTimestamp } from "metabase/lib/time";
-import { getTimelineName } from "metabase/lib/timelines";
+import { getSortedTimelines } from "metabase/lib/timelines";
 import {
   getXValues,
   isTimeseries,
@@ -666,18 +666,16 @@ export const getFetchedTimelines = createSelector([getEntities], entities => {
 export const getTransformedTimelines = createSelector(
   [getFetchedTimelines],
   timelines => {
-    return _.chain(timelines)
-      .map(timeline =>
+    return getSortedTimelines(
+      timelines.map(timeline =>
         updateIn(timeline, ["events"], (events = []) =>
           _.chain(events)
             .map(event => updateIn(event, ["timestamp"], parseTimestamp))
             .filter(event => !event.archived)
             .value(),
         ),
-      )
-      .sortBy(getTimelineName)
-      .sortBy(timeline => timeline.collection?.personal_owner_id != null) // personal collections last
-      .value();
+      ),
+    );
   },
 );
 
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.styled.tsx b/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.styled.tsx
deleted file mode 100644
index 5ff5b4a4491..00000000000
--- a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.styled.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import styled from "@emotion/styled";
-
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
-
-export const ModalFooter = styled.div`
-  display: flex;
-  gap: 1rem;
-  justify-content: flex-end;
-`;
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.tsx b/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.tsx
deleted file mode 100644
index e5a4bebee68..00000000000
--- a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { useCallback } from "react";
-import { t } from "ttag";
-import Button from "metabase/core/components/Button";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
-import { ModalBody, ModalFooter } from "./DeleteEventModal.styled";
-
-export interface DeleteEventModalProps {
-  event: TimelineEvent;
-  timeline: Timeline;
-  collection: Collection;
-  onSubmit: (
-    event: TimelineEvent,
-    timeline: Timeline,
-    collection: Collection,
-  ) => void;
-  onCancel: () => void;
-  onClose?: () => void;
-}
-
-const DeleteEventModal = ({
-  event,
-  timeline,
-  collection,
-  onSubmit,
-  onCancel,
-  onClose,
-}: DeleteEventModalProps): JSX.Element => {
-  const handleSubmit = useCallback(async () => {
-    await onSubmit(event, timeline, collection);
-  }, [event, timeline, collection, onSubmit]);
-
-  return (
-    <div>
-      <ModalHeader title={t`Delete ${event?.name}?`} onClose={onClose} />
-      <ModalBody>
-        <ModalFooter>
-          <Button onClick={onCancel}>{t`Cancel`}</Button>
-          <Button danger onClick={handleSubmit}>{t`Delete`}</Button>
-        </ModalFooter>
-      </ModalBody>
-    </div>
-  );
-};
-
-export default DeleteEventModal;
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.styled.tsx b/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.styled.tsx
deleted file mode 100644
index 5ff5b4a4491..00000000000
--- a/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.styled.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import styled from "@emotion/styled";
-
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
-
-export const ModalFooter = styled.div`
-  display: flex;
-  gap: 1rem;
-  justify-content: flex-end;
-`;
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.tsx b/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.tsx
deleted file mode 100644
index 2284dd22f7e..00000000000
--- a/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/DeleteTimelineModal.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { useCallback } from "react";
-import { t } from "ttag";
-import Button from "metabase/core/components/Button/Button";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline } from "metabase-types/api";
-import { ModalBody, ModalFooter } from "./DeleteTimelineModal.styled";
-
-export interface DeleteTimelineModalProps {
-  timeline: Timeline;
-  collection: Collection;
-  onSubmit: (timeline: Timeline, collection: Collection) => void;
-  onCancel: () => void;
-  onClose: () => void;
-}
-
-const DeleteTimelineModal = ({
-  timeline,
-  collection,
-  onSubmit,
-  onCancel,
-  onClose,
-}: DeleteTimelineModalProps): JSX.Element => {
-  const handleSubmit = useCallback(async () => {
-    await onSubmit(timeline, collection);
-  }, [timeline, collection, onSubmit]);
-
-  return (
-    <div>
-      <ModalHeader title={t`Delete ${timeline?.name}?`} onClose={onClose} />
-      <ModalBody>
-        <ModalFooter>
-          <Button onClick={onCancel}>{t`Cancel`}</Button>
-          <Button danger onClick={handleSubmit}>{t`Delete`}</Button>
-        </ModalFooter>
-      </ModalBody>
-    </div>
-  );
-};
-
-export default DeleteTimelineModal;
diff --git a/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.styled.tsx b/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.styled.tsx
deleted file mode 100644
index 5d29609249e..00000000000
--- a/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.styled.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
-import Button from "metabase/core/components/Button/Button";
-
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
-
-export const ModalDangerButton = styled(Button)`
-  color: ${color("danger")};
-  padding-left: 0;
-  padding-right: 0;
-
-  &:hover {
-    color: ${color("danger")};
-    background-color: transparent;
-  }
-`;
diff --git a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.styled.tsx b/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.styled.tsx
deleted file mode 100644
index 83837d6de31..00000000000
--- a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.styled.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
-import Button from "metabase/core/components/Button";
-
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
-
-export const ModalDangerButton = styled(Button)`
-  color: ${color("danger")};
-  padding-left: 0;
-  padding-right: 0;
-
-  &:hover {
-    color: ${color("danger")};
-    background-color: transparent;
-  }
-`;
diff --git a/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.tsx b/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.tsx
index 947d264c4c9..9542ccc67d7 100644
--- a/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.tsx
+++ b/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.tsx
@@ -6,7 +6,7 @@ import { parseTimestamp } from "metabase/lib/time";
 import { formatDateTimeWithUnit } from "metabase/lib/formatting";
 import Link from "metabase/core/components/Link";
 import EntityMenu from "metabase/components/EntityMenu";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import {
   CardAside,
   CardBody,
@@ -24,7 +24,6 @@ import {
 export interface EventCardProps {
   event: TimelineEvent;
   timeline: Timeline;
-  collection: Collection;
   onArchive?: (event: TimelineEvent) => void;
   onUnarchive?: (event: TimelineEvent) => void;
 }
@@ -32,21 +31,14 @@ export interface EventCardProps {
 const EventCard = ({
   event,
   timeline,
-  collection,
   onArchive,
   onUnarchive,
 }: EventCardProps): JSX.Element => {
-  const menuItems = getMenuItems(
-    event,
-    timeline,
-    collection,
-    onArchive,
-    onUnarchive,
-  );
+  const menuItems = getMenuItems(event, timeline, onArchive, onUnarchive);
   const dateMessage = getDateMessage(event);
   const creatorMessage = getCreatorMessage(event);
   const canEdit = timeline.collection?.can_write && !event.archived;
-  const editLink = Urls.editEventInCollection(event, timeline, collection);
+  const editLink = Urls.editEventInCollection(event, timeline);
 
   return (
     <CardRoot>
@@ -82,7 +74,6 @@ const EventCard = ({
 const getMenuItems = (
   event: TimelineEvent,
   timeline: Timeline,
-  collection: Collection,
   onArchive?: (event: TimelineEvent) => void,
   onUnarchive?: (event: TimelineEvent) => void,
 ) => {
@@ -94,7 +85,11 @@ const getMenuItems = (
     return [
       {
         title: t`Edit event`,
-        link: Urls.editEventInCollection(event, timeline, collection),
+        link: Urls.editEventInCollection(event, timeline),
+      },
+      {
+        title: t`Move event`,
+        link: Urls.moveEventInCollection(event, timeline),
       },
       {
         title: t`Archive event`,
@@ -109,7 +104,7 @@ const getMenuItems = (
       },
       {
         title: t`Delete event`,
-        link: Urls.deleteEventInCollection(event, timeline, collection),
+        link: Urls.deleteEventInCollection(event, timeline),
       },
     ];
   }
diff --git a/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.unit.spec.tsx b/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.unit.spec.tsx
index 1403cef4609..964d14c1609 100644
--- a/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/collections/components/EventCard/EventCard.unit.spec.tsx
@@ -106,7 +106,6 @@ describe("EventCard", () => {
 export const getProps = (opts?: Partial<EventCardProps>): EventCardProps => ({
   event: createMockTimelineEvent(),
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   onArchive: jest.fn(),
   onUnarchive: jest.fn(),
   ...opts,
diff --git a/frontend/src/metabase/timelines/collections/components/EventList/EventList.tsx b/frontend/src/metabase/timelines/collections/components/EventList/EventList.tsx
index 4b78f08593a..ff71b5697e5 100644
--- a/frontend/src/metabase/timelines/collections/components/EventList/EventList.tsx
+++ b/frontend/src/metabase/timelines/collections/components/EventList/EventList.tsx
@@ -1,6 +1,6 @@
 import React, { memo } from "react";
 import { t } from "ttag";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import EventCard from "../EventCard";
 import {
   ListFooter,
@@ -15,7 +15,6 @@ import {
 export interface EventListProps {
   events: TimelineEvent[];
   timeline: Timeline;
-  collection: Collection;
   onArchive?: (event: TimelineEvent) => void;
   onUnarchive?: (event: TimelineEvent) => void;
 }
@@ -23,7 +22,6 @@ export interface EventListProps {
 const EventList = ({
   events,
   timeline,
-  collection,
   onArchive,
   onUnarchive,
 }: EventListProps): JSX.Element => {
@@ -34,7 +32,6 @@ const EventList = ({
           key={event.id}
           event={event}
           timeline={timeline}
-          collection={collection}
           onArchive={onArchive}
           onUnarchive={onUnarchive}
         />
diff --git a/frontend/src/metabase/timelines/collections/components/EventList/EventList.unit.spec.tsx b/frontend/src/metabase/timelines/collections/components/EventList/EventList.unit.spec.tsx
index 98b5e54b594..7116145b0c5 100644
--- a/frontend/src/metabase/timelines/collections/components/EventList/EventList.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/collections/components/EventList/EventList.unit.spec.tsx
@@ -1,7 +1,6 @@
 import React from "react";
 import { render, screen } from "@testing-library/react";
 import {
-  createMockCollection,
   createMockTimeline,
   createMockTimelineEvent,
 } from "metabase-types/api/mocks";
@@ -26,6 +25,5 @@ describe("EventList", () => {
 const getProps = (opts?: Partial<EventListProps>): EventListProps => ({
   events: [],
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   ...opts,
 });
diff --git a/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.tsx
deleted file mode 100644
index 65ec2b34e79..00000000000
--- a/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, { useCallback, useMemo } from "react";
-import { t } from "ttag";
-import { getDefaultTimezone } from "metabase/lib/time";
-import { getDefaultTimelineIcon } from "metabase/lib/timelines";
-import Form from "metabase/containers/Form";
-import forms from "metabase/entities/timeline-events/forms";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
-import { ModalBody } from "./NewEventModal.styled";
-
-export interface NewEventModalProps {
-  timeline?: Timeline;
-  collection: Collection;
-  onSubmit: (
-    values: Partial<TimelineEvent>,
-    collection: Collection,
-    timeline?: Timeline,
-  ) => void;
-  onCancel: (location: string) => void;
-  onClose?: () => void;
-}
-
-const NewEventModal = ({
-  timeline,
-  collection,
-  onSubmit,
-  onCancel,
-  onClose,
-}: NewEventModalProps): JSX.Element => {
-  const form = useMemo(() => forms.details(), []);
-
-  const initialValues = useMemo(
-    () => ({
-      timeline_id: timeline?.id,
-      icon: timeline ? timeline.icon : getDefaultTimelineIcon(),
-      timezone: getDefaultTimezone(),
-      source: "collections",
-      time_matters: false,
-    }),
-    [timeline],
-  );
-
-  const handleSubmit = useCallback(
-    async (values: Partial<TimelineEvent>) => {
-      await onSubmit(values, collection, timeline);
-    },
-    [timeline, collection, onSubmit],
-  );
-
-  return (
-    <div>
-      <ModalHeader title={t`New event`} onClose={onClose} />
-      <ModalBody>
-        <Form
-          form={form}
-          initialValues={initialValues}
-          isModal={true}
-          onSubmit={handleSubmit}
-          onClose={onCancel}
-        />
-      </ModalBody>
-    </div>
-  );
-};
-
-export default NewEventModal;
diff --git a/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.unit.spec.tsx
deleted file mode 100644
index 8d943ba636a..00000000000
--- a/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.unit.spec.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React, { FormHTMLAttributes } from "react";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { createMockCollection } from "metabase-types/api/mocks";
-import NewEventModal, { NewEventModalProps } from "./NewEventModal";
-
-const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
-  <form {...props}>
-    <button>Create</button>
-  </form>
-);
-
-jest.mock("metabase/containers/Form", () => FormMock);
-
-describe("NewEventModal", () => {
-  it("should submit modal", () => {
-    const props = getProps();
-
-    render(<NewEventModal {...props} />);
-    userEvent.click(screen.getByText("Create"));
-
-    expect(props.onSubmit).toHaveBeenCalled();
-  });
-});
-
-const getProps = (opts?: Partial<NewEventModalProps>): NewEventModalProps => ({
-  collection: createMockCollection(),
-  onSubmit: jest.fn(),
-  onCancel: jest.fn(),
-  onClose: jest.fn(),
-  ...opts,
-});
diff --git a/frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.styled.tsx b/frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.styled.tsx
deleted file mode 100644
index 591d8b9a749..00000000000
--- a/frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.styled.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import styled from "@emotion/styled";
-
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.tsx b/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.tsx
index 837bce6d77c..c5914a62453 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.tsx
@@ -1,32 +1,30 @@
 import React, { memo } from "react";
-import { t, msgid, ngettext } from "ttag";
+import { msgid, ngettext, t } from "ttag";
 import * as Urls from "metabase/lib/urls";
-import { getTimelineName } from "metabase/lib/timelines";
+import { getEventCount, getTimelineName } from "metabase/lib/timelines";
 import EntityMenu from "metabase/components/EntityMenu";
-import { Collection, Timeline } from "metabase-types/api";
+import { Timeline } from "metabase-types/api";
 import {
-  CardCount,
   CardBody,
+  CardCount,
   CardDescription,
   CardIcon,
+  CardMenu,
   CardRoot,
   CardTitle,
-  CardMenu,
 } from "./TimelineCard.styled";
 
 export interface TimelineCardProps {
   timeline: Timeline;
-  collection: Collection;
   onUnarchive?: (timeline: Timeline) => void;
 }
 
 const TimelineCard = ({
   timeline,
-  collection,
   onUnarchive,
 }: TimelineCardProps): JSX.Element => {
-  const timelineUrl = Urls.timelineInCollection(timeline, collection);
-  const menuItems = getMenuItems(timeline, collection, onUnarchive);
+  const timelineUrl = Urls.timelineInCollection(timeline);
+  const menuItems = getMenuItems(timeline, onUnarchive);
   const eventCount = getEventCount(timeline);
   const hasDescription = Boolean(timeline.description);
   const hasMenuItems = menuItems.length > 0;
@@ -59,13 +57,8 @@ const TimelineCard = ({
   );
 };
 
-const getEventCount = (timeline: Timeline) => {
-  return timeline.events ? timeline.events.filter(e => !e.archived).length : 0;
-};
-
 const getMenuItems = (
   timeline: Timeline,
-  collection: Collection,
   onUnarchive?: (timeline: Timeline) => void,
 ) => {
   if (!timeline.archived || !timeline.collection?.can_write) {
@@ -79,7 +72,7 @@ const getMenuItems = (
     },
     {
       title: t`Delete timeline`,
-      link: Urls.deleteTimelineInCollection(timeline, collection),
+      link: Urls.deleteTimelineInCollection(timeline),
     },
   ];
 };
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.unit.spec.tsx b/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.unit.spec.tsx
index c05a1292459..1f3a488c8f9 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineCard/TimelineCard.unit.spec.tsx
@@ -1,7 +1,6 @@
 import React from "react";
 import { render, screen } from "@testing-library/react";
 import {
-  createMockCollection,
   createMockTimeline,
   createMockTimelineEvent,
 } from "metabase-types/api/mocks";
@@ -27,6 +26,5 @@ describe("TimelineCard", () => {
 
 const getProps = (opts?: Partial<TimelineCardProps>): TimelineCardProps => ({
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   ...opts,
 });
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.tsx b/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.tsx
index 18f5314d0ae..1e4054d4e08 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.tsx
@@ -9,7 +9,7 @@ import { useDebouncedValue } from "metabase/hooks/use-debounced-value";
 import Icon from "metabase/components/Icon";
 import EntityMenu from "metabase/components/EntityMenu";
 import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import SearchEmptyState from "../SearchEmptyState";
 import EventList from "../EventList";
 import TimelineEmptyState from "../TimelineEmptyState";
@@ -24,18 +24,16 @@ import { MenuItem } from "../../types";
 
 export interface TimelineDetailsModalProps {
   timeline: Timeline;
-  collection: Collection;
   isArchive?: boolean;
   isOnlyTimeline?: boolean;
   onArchive?: (event: TimelineEvent) => void;
   onUnarchive?: (event: TimelineEvent) => void;
   onClose?: () => void;
-  onGoBack?: (timeline: Timeline, collection: Collection) => void;
+  onGoBack?: (timeline: Timeline) => void;
 }
 
 const TimelineDetailsModal = ({
   timeline,
-  collection,
   isArchive = false,
   isOnlyTimeline = false,
   onArchive,
@@ -56,12 +54,12 @@ const TimelineDetailsModal = ({
   }, [timeline, searchText, isArchive]);
 
   const menuItems = useMemo(() => {
-    return getMenuItems(timeline, collection, isArchive, isOnlyTimeline);
-  }, [timeline, collection, isArchive, isOnlyTimeline]);
+    return getMenuItems(timeline, isArchive, isOnlyTimeline);
+  }, [timeline, isArchive, isOnlyTimeline]);
 
   const handleGoBack = useCallback(() => {
-    onGoBack?.(timeline, collection);
-  }, [timeline, collection, onGoBack]);
+    onGoBack?.(timeline);
+  }, [timeline, onGoBack]);
 
   const isNotEmpty = events.length > 0;
   const isSearching = searchText.length > 0;
@@ -90,7 +88,7 @@ const TimelineDetailsModal = ({
           {canWrite && !isArchive && (
             <ModalToolbarLink
               className="Button"
-              to={Urls.newEventInCollection(timeline, collection)}
+              to={Urls.newEventInCollection(timeline)}
             >{t`Add an event`}</ModalToolbarLink>
           )}
         </ModalToolbar>
@@ -100,14 +98,13 @@ const TimelineDetailsModal = ({
           <EventList
             events={events}
             timeline={timeline}
-            collection={collection}
             onArchive={onArchive}
             onUnarchive={onUnarchive}
           />
         ) : isArchive || isSearching ? (
           <SearchEmptyState />
         ) : (
-          <TimelineEmptyState timeline={timeline} collection={collection} />
+          <TimelineEmptyState timeline={timeline} />
         )}
       </ModalBody>
     </ModalRoot>
@@ -139,7 +136,6 @@ const isEventMatch = (event: TimelineEvent, searchText: string) => {
 
 const getMenuItems = (
   timeline: Timeline,
-  collection: Collection,
   isArchive: boolean,
   isOnlyTimeline: boolean,
 ) => {
@@ -149,11 +145,11 @@ const getMenuItems = (
     items.push(
       {
         title: t`New timeline`,
-        link: Urls.newTimelineInCollection(collection),
+        link: Urls.newTimelineInCollection(timeline.collection),
       },
       {
         title: t`Edit timeline details`,
-        link: Urls.editTimelineInCollection(timeline, collection),
+        link: Urls.editTimelineInCollection(timeline),
       },
     );
   }
@@ -161,14 +157,14 @@ const getMenuItems = (
   if (!isArchive) {
     items.push({
       title: t`View archived events`,
-      link: Urls.timelineArchiveInCollection(timeline, collection),
+      link: Urls.timelineArchiveInCollection(timeline),
     });
   }
 
   if (isOnlyTimeline) {
     items.push({
       title: t`View archived timelines`,
-      link: Urls.timelinesArchiveInCollection(collection),
+      link: Urls.timelinesArchiveInCollection(timeline.collection),
     });
   }
 
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx b/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx
index 5615364ecc7..c7eea9ce4f5 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineDetailsModal/TimelineDetailsModal.unit.spec.tsx
@@ -75,6 +75,5 @@ const getProps = (
   opts?: Partial<TimelineDetailsModalProps>,
 ): TimelineDetailsModalProps => ({
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   ...opts,
 });
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineEmptyState/TimelineEmptyState.tsx b/frontend/src/metabase/timelines/collections/components/TimelineEmptyState/TimelineEmptyState.tsx
index a1054d2d2d5..6a870cef7f7 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineEmptyState/TimelineEmptyState.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineEmptyState/TimelineEmptyState.tsx
@@ -22,7 +22,7 @@ import {
 
 export interface TimelineEmptyStateProps {
   timeline?: Timeline;
-  collection: Collection;
+  collection?: Collection;
 }
 
 const TimelineEmptyState = ({
@@ -31,11 +31,11 @@ const TimelineEmptyState = ({
 }: TimelineEmptyStateProps): JSX.Element => {
   const date = moment();
   const link = timeline
-    ? Urls.newEventInCollection(timeline, collection)
+    ? Urls.newEventInCollection(timeline)
     : Urls.newEventAndTimelineInCollection(collection);
   const canWrite = timeline
     ? timeline.collection?.can_write
-    : collection.can_write;
+    : collection?.can_write;
 
   return (
     <EmptyStateRoot>
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineList/TimelineList.tsx b/frontend/src/metabase/timelines/collections/components/TimelineList/TimelineList.tsx
index 703e8834c19..a4ccea87ae4 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineList/TimelineList.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineList/TimelineList.tsx
@@ -1,17 +1,15 @@
 import React from "react";
-import { Collection, Timeline } from "metabase-types/api";
+import { Timeline } from "metabase-types/api";
 import TimelineCard from "../TimelineCard";
 import { ListRoot } from "./TimelineList.styled";
 
 export interface TimelineListProps {
   timelines: Timeline[];
-  collection: Collection;
   onUnarchive?: (timeline: Timeline) => void;
 }
 
 const TimelineList = ({
   timelines,
-  collection,
   onUnarchive,
 }: TimelineListProps): JSX.Element => {
   return (
@@ -20,7 +18,6 @@ const TimelineList = ({
         <TimelineCard
           key={timeline.id}
           timeline={timeline}
-          collection={collection}
           onUnarchive={onUnarchive}
         />
       ))}
diff --git a/frontend/src/metabase/timelines/collections/components/TimelineListModal/TimelineListModal.tsx b/frontend/src/metabase/timelines/collections/components/TimelineListModal/TimelineListModal.tsx
index 60bf887a031..3cd27b1c016 100644
--- a/frontend/src/metabase/timelines/collections/components/TimelineListModal/TimelineListModal.tsx
+++ b/frontend/src/metabase/timelines/collections/components/TimelineListModal/TimelineListModal.tsx
@@ -1,10 +1,9 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useMemo } from "react";
 import { t } from "ttag";
-import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import {
   getDefaultTimelineName,
-  getTimelineName,
+  getSortedTimelines,
 } from "metabase/lib/timelines";
 import EntityMenu from "metabase/components/EntityMenu";
 import { Collection, Timeline } from "metabase-types/api";
@@ -33,10 +32,13 @@ const TimelineListModal = ({
 }: TimelineListModalProps): JSX.Element => {
   const title = getTitle(timelines, collection, isArchive);
   const menuItems = getMenuItems(timelines, collection, isArchive);
-  const sortedTimelines = getSortedTimelines(timelines);
   const hasTimelines = timelines.length > 0;
   const hasMenuItems = menuItems.length > 0;
 
+  const sortedTimelines = useMemo(() => {
+    return getSortedTimelines(timelines, collection);
+  }, [timelines, collection]);
+
   const handleGoBack = useCallback(() => {
     onGoBack?.(collection);
   }, [collection, onGoBack]);
@@ -54,11 +56,7 @@ const TimelineListModal = ({
       </ModalHeader>
       <ModalBody isTopAligned={hasTimelines}>
         {hasTimelines ? (
-          <TimelineList
-            timelines={sortedTimelines}
-            collection={collection}
-            onUnarchive={onUnarchive}
-          />
+          <TimelineList timelines={sortedTimelines} onUnarchive={onUnarchive} />
         ) : isArchive ? (
           <SearchEmptyState isTimeline={isArchive} />
         ) : (
@@ -104,11 +102,4 @@ const getMenuItems = (
   ];
 };
 
-const getSortedTimelines = (timelines: Timeline[]) => {
-  return _.chain(timelines)
-    .sortBy(getTimelineName)
-    .sortBy(timeline => timeline.collection?.personal_owner_id != null) // personal collections last
-    .value();
-};
-
 export default TimelineListModal;
diff --git a/frontend/src/metabase/timelines/collections/containers/DeleteEventModal/DeleteEventModal.tsx b/frontend/src/metabase/timelines/collections/containers/DeleteEventModal/DeleteEventModal.tsx
index 354bfb13671..6578957bc2b 100644
--- a/frontend/src/metabase/timelines/collections/containers/DeleteEventModal/DeleteEventModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/DeleteEventModal/DeleteEventModal.tsx
@@ -2,39 +2,33 @@ import { connect } from "react-redux";
 import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
-import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import DeleteEventModal from "metabase/timelines/common/components/DeleteEventModal";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import DeleteEventModal from "../../components/DeleteEventModal";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface DeleteEventModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: DeleteEventModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
 };
 
 const timelineEventProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: DeleteEventModalProps) =>
     Urls.extractEntityId(props.params.timelineEventId),
   entityAlias: "event",
 };
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-};
-
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (
-    event: TimelineEvent,
-    timeline: Timeline,
-    collection: Collection,
-  ) => {
+  onSubmit: async (event: TimelineEvent, timeline: Timeline) => {
     await dispatch(TimelineEvents.actions.delete(event));
-    dispatch(push(Urls.timelineArchiveInCollection(timeline, collection)));
+    dispatch(push(Urls.timelineArchiveInCollection(timeline)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -44,6 +38,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 export default _.compose(
   Timelines.load(timelineProps),
   TimelineEvents.load(timelineEventProps),
-  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(DeleteEventModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/DeleteTimelineModal/DeleteTimelineModal.tsx b/frontend/src/metabase/timelines/collections/containers/DeleteTimelineModal/DeleteTimelineModal.tsx
index 8d74c6f3c9f..9b80fcf1bc5 100644
--- a/frontend/src/metabase/timelines/collections/containers/DeleteTimelineModal/DeleteTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/DeleteTimelineModal/DeleteTimelineModal.tsx
@@ -3,27 +3,25 @@ import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import Timelines from "metabase/entities/timelines";
-import Collections from "metabase/entities/collections";
-import { Collection, Timeline } from "metabase-types/api";
+import { Timeline } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import DeleteTimelineModal from "../../components/DeleteTimelineModal";
-import { ModalProps } from "../../types";
+import DeleteTimelineModal from "metabase/timelines/common/components/DeleteTimelineModal";
+import { ModalParams } from "../../types";
+
+interface DeleteTimelineModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: DeleteTimelineModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
 };
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-};
-
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (timeline: Timeline, collection: Collection) => {
+  onSubmit: async (timeline: Timeline) => {
     await dispatch(Timelines.actions.delete(timeline));
-    dispatch(push(Urls.timelinesArchiveInCollection(collection)));
+    dispatch(push(Urls.timelinesArchiveInCollection(timeline.collection)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -32,6 +30,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 
 export default _.compose(
   Timelines.load(timelineProps),
-  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(DeleteTimelineModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/collections/containers/EditEventModal/EditEventModal.tsx
index 32963d2817f..36f59412592 100644
--- a/frontend/src/metabase/timelines/collections/containers/EditEventModal/EditEventModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/EditEventModal/EditEventModal.tsx
@@ -2,50 +2,40 @@ import { connect } from "react-redux";
 import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
-import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import EditEventModal from "metabase/timelines/common/components/EditEventModal";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import EditEventModal from "../../components/EditEventModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface EditEventModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: EditEventModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
+  LoadingAndErrorWrapper,
 };
 
 const timelineEventProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: EditEventModalProps) =>
     Urls.extractEntityId(props.params.timelineEventId),
   entityAlias: "event",
   LoadingAndErrorWrapper,
 };
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-  LoadingAndErrorWrapper,
-};
-
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (
-    event: TimelineEvent,
-    timeline: Timeline,
-    collection: Collection,
-  ) => {
+  onSubmit: async (event: TimelineEvent, timeline?: Timeline) => {
     await dispatch(TimelineEvents.actions.update(event));
-    dispatch(push(Urls.timelineInCollection(timeline, collection)));
+    timeline && dispatch(push(Urls.timelineInCollection(timeline)));
   },
-  onArchive: async (
-    event: TimelineEvent,
-    timeline: Timeline,
-    collection: Collection,
-  ) => {
+  onArchive: async (event: TimelineEvent, timeline?: Timeline) => {
     await dispatch(TimelineEvents.actions.setArchived(event, true));
-    dispatch(push(Urls.timelineInCollection(timeline, collection)));
+    timeline && dispatch(push(Urls.timelineInCollection(timeline)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -55,6 +45,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 export default _.compose(
   Timelines.load(timelineProps),
   TimelineEvents.load(timelineEventProps),
-  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(EditEventModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/EditTimelineModal/EditTimelineModal.tsx b/frontend/src/metabase/timelines/collections/containers/EditTimelineModal/EditTimelineModal.tsx
index 3529ef0df65..dbb5a599256 100644
--- a/frontend/src/metabase/timelines/collections/containers/EditTimelineModal/EditTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/EditTimelineModal/EditTimelineModal.tsx
@@ -2,35 +2,32 @@ import { connect } from "react-redux";
 import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
-import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
-import { Collection, Timeline } from "metabase-types/api";
+import EditTimelineModal from "metabase/timelines/common/components/EditTimelineModal";
+import { Timeline } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import EditTimelineModal from "../../components/EditTimelineModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface EditTimelineModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: EditTimelineModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
   LoadingAndErrorWrapper,
 };
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-  LoadingAndErrorWrapper,
-};
-
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (timeline: Timeline, collection: Collection) => {
+  onSubmit: async (timeline: Timeline) => {
     await dispatch(Timelines.actions.update(timeline));
-    dispatch(push(Urls.timelineInCollection(timeline, collection)));
+    dispatch(push(Urls.timelineInCollection(timeline)));
   },
-  onArchive: async (timeline: Timeline, collection: Collection) => {
+  onArchive: async (timeline: Timeline) => {
     await dispatch(Timelines.actions.setArchived(timeline, true));
-    dispatch(push(Urls.timelinesInCollection(collection)));
+    dispatch(push(Urls.timelinesInCollection(timeline.collection)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -39,6 +36,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 
 export default _.compose(
   Timelines.load(timelineProps),
-  Collections.load(collectionProps),
   connect(null, mapDispatchToProps),
 )(EditTimelineModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/MoveEventModal/MoveEventModal.tsx b/frontend/src/metabase/timelines/collections/containers/MoveEventModal/MoveEventModal.tsx
new file mode 100644
index 00000000000..bf4a9744d78
--- /dev/null
+++ b/frontend/src/metabase/timelines/collections/containers/MoveEventModal/MoveEventModal.tsx
@@ -0,0 +1,55 @@
+import { connect } from "react-redux";
+import { goBack, push } from "react-router-redux";
+import _ from "underscore";
+import * as Urls from "metabase/lib/urls";
+import Collections from "metabase/entities/collections";
+import Timelines from "metabase/entities/timelines";
+import TimelineEvents from "metabase/entities/timeline-events";
+import MoveEventModal from "metabase/timelines/common/components/MoveEventModal";
+import { Timeline, TimelineEvent } from "metabase-types/api";
+import { State } from "metabase-types/store";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
+import { ModalParams } from "../../types";
+
+interface MoveEventModalProps {
+  params: ModalParams;
+}
+
+const timelinesProps = {
+  query: { include: "events" },
+  LoadingAndErrorWrapper,
+};
+
+const timelineEventProps = {
+  id: (state: State, props: MoveEventModalProps) =>
+    Urls.extractEntityId(props.params.timelineEventId),
+  entityAlias: "event",
+  LoadingAndErrorWrapper,
+};
+
+const collectionProps = {
+  id: (state: State, props: MoveEventModalProps) =>
+    Urls.extractCollectionId(props.params.slug),
+  LoadingAndErrorWrapper,
+};
+
+const mapDispatchToProps = (dispatch: any) => ({
+  onSubmit: async (
+    event: TimelineEvent,
+    newTimeline: Timeline,
+    oldTimeline: Timeline,
+  ) => {
+    await dispatch(TimelineEvents.actions.setTimeline(event, newTimeline));
+    dispatch(push(Urls.timelineInCollection(oldTimeline)));
+  },
+  onCancel: () => {
+    dispatch(goBack());
+  },
+});
+
+export default _.compose(
+  Timelines.loadList(timelinesProps),
+  TimelineEvents.load(timelineEventProps),
+  Collections.load(collectionProps),
+  connect(null, mapDispatchToProps),
+)(MoveEventModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/MoveEventModal/index.ts b/frontend/src/metabase/timelines/collections/containers/MoveEventModal/index.ts
new file mode 100644
index 00000000000..369d23ed2ee
--- /dev/null
+++ b/frontend/src/metabase/timelines/collections/containers/MoveEventModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./MoveEventModal";
diff --git a/frontend/src/metabase/timelines/collections/containers/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/collections/containers/NewEventModal/NewEventModal.tsx
index fa83471b4d7..4b360e78b67 100644
--- a/frontend/src/metabase/timelines/collections/containers/NewEventModal/NewEventModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/NewEventModal/NewEventModal.tsx
@@ -2,27 +2,30 @@ import { connect } from "react-redux";
 import { goBack, push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
-import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
+import NewEventModal from "metabase/timelines/common/components/NewEventModal";
 import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import NewEventModal from "../../components/NewEventModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface NewEventModalProps {
+  params: ModalParams;
+  timeline: Timeline;
+}
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: NewEventModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
   LoadingAndErrorWrapper,
 };
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-  LoadingAndErrorWrapper,
-};
+const mapStateToProps = (state: State, { timeline }: NewEventModalProps) => ({
+  source: "collections",
+  timelines: [timeline],
+});
 
 const mapDispatchToProps = (dispatch: any) => ({
   onSubmit: async (
@@ -31,7 +34,7 @@ const mapDispatchToProps = (dispatch: any) => ({
     timeline: Timeline,
   ) => {
     await dispatch(TimelineEvents.actions.create(values));
-    dispatch(push(Urls.timelineInCollection(timeline, collection)));
+    dispatch(push(Urls.timelineInCollection(timeline)));
   },
   onCancel: () => {
     dispatch(goBack());
@@ -40,6 +43,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 
 export default _.compose(
   Timelines.load(timelineProps),
-  Collections.load(collectionProps),
-  connect(null, mapDispatchToProps),
+  connect(mapStateToProps, mapDispatchToProps),
 )(NewEventModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx b/frontend/src/metabase/timelines/collections/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
index c304021085a..6019e060838 100644
--- a/frontend/src/metabase/timelines/collections/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/NewEventWithTimelineModal/NewEventWithTimelineModal.tsx
@@ -4,14 +4,18 @@ import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
+import NewEventModal from "metabase/timelines/common/components/NewEventModal";
 import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import NewEventModal from "../../components/NewEventModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface NewEventWithTimelineModalProps {
+  params: ModalParams;
+}
 
 const collectionProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: NewEventWithTimelineModalProps) =>
     Urls.extractCollectionId(props.params.slug),
   LoadingAndErrorWrapper,
 };
diff --git a/frontend/src/metabase/timelines/collections/containers/NewTimelineModal/NewTimelineModal.tsx b/frontend/src/metabase/timelines/collections/containers/NewTimelineModal/NewTimelineModal.tsx
index 64fc36693f1..41131a4710f 100644
--- a/frontend/src/metabase/timelines/collections/containers/NewTimelineModal/NewTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/NewTimelineModal/NewTimelineModal.tsx
@@ -4,24 +4,28 @@ import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
 import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
-import { Collection, Timeline } from "metabase-types/api";
+import NewTimelineModal from "metabase/timelines/common/components/NewTimelineModal";
+import { Timeline } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import NewTimelineModal from "../../components/NewTimelineModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface NewTimelineModalProps {
+  params: ModalParams;
+}
 
 const collectionProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: NewTimelineModalProps) =>
     Urls.extractCollectionId(props.params.slug),
   LoadingAndErrorWrapper,
 };
 
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (values: Partial<Timeline>, collection: Collection) => {
+  onSubmit: async (values: Partial<Timeline>) => {
     const action = Timelines.actions.create(values);
     const response = await dispatch(action);
     const timeline = Timelines.HACK_getObjectFromAction(response);
-    dispatch(push(Urls.timelineInCollection(timeline, collection)));
+    dispatch(push(Urls.timelineInCollection(timeline)));
   },
   onCancel: () => {
     dispatch(goBack());
diff --git a/frontend/src/metabase/timelines/collections/containers/TimelineArchiveModal/TimelineArchiveModal.tsx b/frontend/src/metabase/timelines/collections/containers/TimelineArchiveModal/TimelineArchiveModal.tsx
index aff7fc4fa4b..8481aaeae42 100644
--- a/frontend/src/metabase/timelines/collections/containers/TimelineArchiveModal/TimelineArchiveModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/TimelineArchiveModal/TimelineArchiveModal.tsx
@@ -2,28 +2,25 @@ import { connect } from "react-redux";
 import { push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
-import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import TimelineDetailsModal from "../../components/TimelineDetailsModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
-import { ModalProps } from "../../types";
+import { ModalParams } from "../../types";
+
+interface TimelineArchiveModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: TimelineArchiveModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events", archived: true },
   LoadingAndErrorWrapper,
 };
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-  LoadingAndErrorWrapper,
-};
-
 const mapStateToProps = () => ({
   isArchive: true,
 });
@@ -32,13 +29,12 @@ const mapDispatchToProps = (dispatch: any) => ({
   onUnarchive: async (event: TimelineEvent) => {
     await dispatch(TimelineEvents.actions.setArchived(event, false));
   },
-  onGoBack: (timeline: Timeline, collection: Collection) => {
-    dispatch(push(Urls.timelineInCollection(timeline, collection)));
+  onGoBack: (timeline: Timeline) => {
+    dispatch(push(Urls.timelineInCollection(timeline)));
   },
 });
 
 export default _.compose(
   Timelines.load(timelineProps),
-  Collections.load(collectionProps),
   connect(mapStateToProps, mapDispatchToProps),
 )(TimelineDetailsModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/TimelineDetailsModal/TimelineDetailsModal.tsx b/frontend/src/metabase/timelines/collections/containers/TimelineDetailsModal/TimelineDetailsModal.tsx
index e810aa08e6a..aeaf1e8ac4d 100644
--- a/frontend/src/metabase/timelines/collections/containers/TimelineDetailsModal/TimelineDetailsModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/TimelineDetailsModal/TimelineDetailsModal.tsx
@@ -2,7 +2,6 @@ import { connect } from "react-redux";
 import { push } from "react-router-redux";
 import _ from "underscore";
 import * as Urls from "metabase/lib/urls";
-import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
 import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
@@ -11,34 +10,29 @@ import TimelineDetailsModal from "../../components/TimelineDetailsModal";
 import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
 import { ModalParams } from "../../types";
 
-interface ModalProps {
+interface TimelineDetailsModalProps {
   params: ModalParams;
   timelines: Timeline[];
 }
 
 const timelineProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: TimelineDetailsModalProps) =>
     Urls.extractEntityId(props.params.timelineId),
   query: { include: "events" },
   LoadingAndErrorWrapper,
 };
 
 const timelinesProps = {
-  query: (state: State, props: ModalProps) => ({
+  query: (state: State, props: TimelineDetailsModalProps) => ({
     collectionId: Urls.extractCollectionId(props.params.slug),
   }),
   LoadingAndErrorWrapper,
 };
 
-const mapStateToProps = (state: State, { timelines }: ModalProps) => ({
-  isOnlyTimeline: timelines.length === 1,
+const mapStateToProps = (state: State, props: TimelineDetailsModalProps) => ({
+  isOnlyTimeline: props.timelines.length === 1,
 });
 
-const collectionProps = {
-  id: (state: State, props: ModalProps) =>
-    Urls.extractCollectionId(props.params.slug),
-};
-
 const mapDispatchToProps = (dispatch: any) => ({
   onArchive: async (event: TimelineEvent) => {
     await dispatch(TimelineEvents.actions.setArchived(event, true));
@@ -51,6 +45,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 export default _.compose(
   Timelines.load(timelineProps),
   Timelines.loadList(timelinesProps),
-  Collections.load(collectionProps),
   connect(mapStateToProps, mapDispatchToProps),
 )(TimelineDetailsModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/TimelineIndexModal/TimelineIndexModal.tsx b/frontend/src/metabase/timelines/collections/containers/TimelineIndexModal/TimelineIndexModal.tsx
index 9a0316b370c..66dda2ffebc 100644
--- a/frontend/src/metabase/timelines/collections/containers/TimelineIndexModal/TimelineIndexModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/TimelineIndexModal/TimelineIndexModal.tsx
@@ -2,13 +2,19 @@ import * as Urls from "metabase/lib/urls";
 import Timelines from "metabase/entities/timelines";
 import { State } from "metabase-types/store";
 import TimelineIndexModal from "../../components/TimelineIndexModal";
-import { ModalProps } from "../../types";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
+import { ModalParams } from "../../types";
+
+interface TimelineIndexModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  query: (state: State, props: ModalProps) => ({
+  query: (state: State, props: TimelineIndexModalProps) => ({
     collectionId: Urls.extractCollectionId(props.params.slug),
     include: "events",
   }),
+  LoadingAndErrorWrapper,
 };
 
 export default Timelines.loadList(timelineProps)(TimelineIndexModal);
diff --git a/frontend/src/metabase/timelines/collections/containers/TimelineListArchiveModal/TimelineListArchiveModal.tsx b/frontend/src/metabase/timelines/collections/containers/TimelineListArchiveModal/TimelineListArchiveModal.tsx
index 502cd816d32..286290e6bce 100644
--- a/frontend/src/metabase/timelines/collections/containers/TimelineListArchiveModal/TimelineListArchiveModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/TimelineListArchiveModal/TimelineListArchiveModal.tsx
@@ -7,19 +7,26 @@ import Timelines from "metabase/entities/timelines";
 import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
 import TimelineListModal from "../../components/TimelineListModal";
-import { ModalProps } from "../../types";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
+import { ModalParams } from "../../types";
+
+interface TimelineListArchiveModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  query: (state: State, props: ModalProps) => ({
+  query: (state: State, props: TimelineListArchiveModalProps) => ({
     collectionId: Urls.extractCollectionId(props.params.slug),
     archived: true,
     include: "events",
   }),
+  LoadingAndErrorWrapper,
 };
 
 const collectionProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: TimelineListArchiveModalProps) =>
     Urls.extractCollectionId(props.params.slug),
+  LoadingAndErrorWrapper,
 };
 
 const mapStateToProps = () => ({
diff --git a/frontend/src/metabase/timelines/collections/containers/TimelineListModal/TimelineListModal.tsx b/frontend/src/metabase/timelines/collections/containers/TimelineListModal/TimelineListModal.tsx
index 01f1bb87604..8c06ec65902 100644
--- a/frontend/src/metabase/timelines/collections/containers/TimelineListModal/TimelineListModal.tsx
+++ b/frontend/src/metabase/timelines/collections/containers/TimelineListModal/TimelineListModal.tsx
@@ -4,18 +4,25 @@ import Collections from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import { State } from "metabase-types/store";
 import TimelineListModal from "../../components/TimelineListModal";
-import { ModalProps } from "../../types";
+import LoadingAndErrorWrapper from "../../components/LoadingAndErrorWrapper";
+import { ModalParams } from "../../types";
+
+interface TimelineListModalProps {
+  params: ModalParams;
+}
 
 const timelineProps = {
-  query: (state: State, props: ModalProps) => ({
+  query: (state: State, props: TimelineListModalProps) => ({
     collectionId: Urls.extractCollectionId(props.params.slug),
     include: "events",
   }),
+  LoadingAndErrorWrapper,
 };
 
 const collectionProps = {
-  id: (state: State, props: ModalProps) =>
+  id: (state: State, props: TimelineListModalProps) =>
     Urls.extractCollectionId(props.params.slug),
+  LoadingAndErrorWrapper,
 };
 
 export default _.compose(
diff --git a/frontend/src/metabase/timelines/collections/routes.tsx b/frontend/src/metabase/timelines/collections/routes.tsx
index 32abd01b77f..c02fe6658be 100644
--- a/frontend/src/metabase/timelines/collections/routes.tsx
+++ b/frontend/src/metabase/timelines/collections/routes.tsx
@@ -4,6 +4,7 @@ import DeleteEventModal from "./containers/DeleteEventModal";
 import DeleteTimelineModal from "./containers/DeleteTimelineModal";
 import EditEventModal from "./containers/EditEventModal";
 import EditTimelineModal from "./containers/EditTimelineModal";
+import MoveEventModal from "./containers/MoveEventModal";
 import NewEventModal from "./containers/NewEventModal";
 import NewEventWithTimelineModal from "./containers/NewEventWithTimelineModal";
 import NewTimelineModal from "./containers/NewTimelineModal";
@@ -85,6 +86,13 @@ const getRoutes = () => {
           modalProps: { enableTransition: false },
         }}
       />
+      <ModalRoute
+        {...{
+          path: "timelines/:timelineId/events/:timelineEventId/move",
+          modal: MoveEventModal,
+          modalProps: { enableTransition: false },
+        }}
+      />
       <ModalRoute
         {...{
           path: "timelines/:timelineId/events/:timelineEventId/delete",
diff --git a/frontend/src/metabase/timelines/collections/types.ts b/frontend/src/metabase/timelines/collections/types.ts
index 3ca07dee24b..1e7f0bc2f34 100644
--- a/frontend/src/metabase/timelines/collections/types.ts
+++ b/frontend/src/metabase/timelines/collections/types.ts
@@ -9,7 +9,3 @@ export interface ModalParams {
   timelineId?: string;
   timelineEventId?: string;
 }
-
-export interface ModalProps {
-  params: ModalParams;
-}
diff --git a/frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.tsx b/frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.tsx
new file mode 100644
index 00000000000..136c7220170
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.tsx
@@ -0,0 +1,41 @@
+import React, { useCallback } from "react";
+import { t } from "ttag";
+import Button from "metabase/core/components/Button";
+import { Timeline, TimelineEvent } from "metabase-types/api";
+import ModalHeader from "../ModalHeader";
+import ModalFooter from "../ModalFooter";
+
+export interface DeleteEventModalProps {
+  event: TimelineEvent;
+  timeline: Timeline;
+  onSubmit: (event: TimelineEvent, timeline: Timeline) => void;
+  onSubmitSuccess?: () => void;
+  onCancel?: () => void;
+  onClose?: () => void;
+}
+
+const DeleteEventModal = ({
+  event,
+  timeline,
+  onSubmit,
+  onSubmitSuccess,
+  onCancel,
+  onClose,
+}: DeleteEventModalProps): JSX.Element => {
+  const handleSubmit = useCallback(async () => {
+    await onSubmit(event, timeline);
+    onSubmitSuccess?.();
+  }, [event, timeline, onSubmit, onSubmitSuccess]);
+
+  return (
+    <div>
+      <ModalHeader title={t`Delete ${event?.name}?`} onClose={onClose} />
+      <ModalFooter hasPadding>
+        <Button onClick={onCancel}>{t`Cancel`}</Button>
+        <Button danger onClick={handleSubmit}>{t`Delete`}</Button>
+      </ModalFooter>
+    </div>
+  );
+};
+
+export default DeleteEventModal;
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
similarity index 92%
rename from frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
rename to frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
index 51200403351..5e1cf23061e 100644
--- a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/common/components/DeleteEventModal/DeleteEventModal.unit.spec.tsx
@@ -2,7 +2,6 @@ import React from "react";
 import { render, screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import {
-  createMockCollection,
   createMockTimeline,
   createMockTimelineEvent,
 } from "metabase-types/api/mocks";
@@ -24,7 +23,6 @@ const getProps = (
 ): DeleteEventModalProps => ({
   event: createMockTimelineEvent(),
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   onSubmit: jest.fn(),
   onCancel: jest.fn(),
   onClose: jest.fn(),
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteEventModal/index.ts b/frontend/src/metabase/timelines/common/components/DeleteEventModal/index.ts
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/DeleteEventModal/index.ts
rename to frontend/src/metabase/timelines/common/components/DeleteEventModal/index.ts
diff --git a/frontend/src/metabase/timelines/common/components/DeleteTimelineModal/DeleteTimelineModal.tsx b/frontend/src/metabase/timelines/common/components/DeleteTimelineModal/DeleteTimelineModal.tsx
new file mode 100644
index 00000000000..e76832743ab
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/DeleteTimelineModal/DeleteTimelineModal.tsx
@@ -0,0 +1,39 @@
+import React, { useCallback } from "react";
+import { t } from "ttag";
+import Button from "metabase/core/components/Button/Button";
+import { Timeline } from "metabase-types/api";
+import ModalHeader from "../ModalHeader";
+import ModalFooter from "../ModalFooter";
+
+export interface DeleteTimelineModalProps {
+  timeline: Timeline;
+  onSubmit: (timeline: Timeline) => void;
+  onSubmitSuccess?: () => void;
+  onCancel?: () => void;
+  onClose?: () => void;
+}
+
+const DeleteTimelineModal = ({
+  timeline,
+  onSubmit,
+  onSubmitSuccess,
+  onCancel,
+  onClose,
+}: DeleteTimelineModalProps): JSX.Element => {
+  const handleSubmit = useCallback(async () => {
+    await onSubmit(timeline);
+    onSubmitSuccess?.();
+  }, [timeline, onSubmit, onSubmitSuccess]);
+
+  return (
+    <div>
+      <ModalHeader title={t`Delete ${timeline?.name}?`} onClose={onClose} />
+      <ModalFooter hasPadding>
+        <Button onClick={onCancel}>{t`Cancel`}</Button>
+        <Button danger onClick={handleSubmit}>{t`Delete`}</Button>
+      </ModalFooter>
+    </div>
+  );
+};
+
+export default DeleteTimelineModal;
diff --git a/frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/index.ts b/frontend/src/metabase/timelines/common/components/DeleteTimelineModal/index.ts
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/DeleteTimelineModal/index.ts
rename to frontend/src/metabase/timelines/common/components/DeleteTimelineModal/index.ts
diff --git a/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx
similarity index 57%
rename from frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.tsx
rename to frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx
index e1ce0910b74..6df46fadbe1 100644
--- a/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.tsx
+++ b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.tsx
@@ -2,34 +2,29 @@ import React, { useCallback, useMemo } from "react";
 import { t } from "ttag";
 import Form from "metabase/containers/Form";
 import forms from "metabase/entities/timeline-events/forms";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
-import { ModalBody, ModalDangerButton } from "./EditEventModal.styled";
+import { Timeline, TimelineEvent } from "metabase-types/api";
+import ModalBody from "../ModalBody";
+import ModalDangerButton from "../ModalDangerButton";
+import ModalHeader from "../ModalHeader";
 
 export interface EditEventModalProps {
   event: TimelineEvent;
-  timeline: Timeline;
-  collection: Collection;
-  onSubmit: (
-    event: TimelineEvent,
-    timeline: Timeline,
-    collection: Collection,
-  ) => void;
-  onArchive: (
-    event: TimelineEvent,
-    timeline: Timeline,
-    collection: Collection,
-  ) => void;
-  onCancel: () => void;
+  timeline?: Timeline;
+  onSubmit: (event: TimelineEvent, timeline?: Timeline) => void;
+  onSubmitSuccess?: () => void;
+  onArchive: (event: TimelineEvent, timeline?: Timeline) => void;
+  onArchiveSuccess?: () => void;
+  onCancel?: () => void;
   onClose?: () => void;
 }
 
 const EditEventModal = ({
   event,
   timeline,
-  collection,
   onSubmit,
+  onSubmitSuccess,
   onArchive,
+  onArchiveSuccess,
   onCancel,
   onClose,
 }: EditEventModalProps): JSX.Element => {
@@ -37,14 +32,16 @@ const EditEventModal = ({
 
   const handleSubmit = useCallback(
     async (event: TimelineEvent) => {
-      await onSubmit(event, timeline, collection);
+      await onSubmit(event, timeline);
+      onSubmitSuccess?.();
     },
-    [timeline, collection, onSubmit],
+    [timeline, onSubmit, onSubmitSuccess],
   );
 
   const handleArchive = useCallback(async () => {
-    await onArchive(event, timeline, collection);
-  }, [event, timeline, collection, onArchive]);
+    await onArchive(event, timeline);
+    onArchiveSuccess?.();
+  }, [event, timeline, onArchive, onArchiveSuccess]);
 
   return (
     <div>
@@ -57,7 +54,7 @@ const EditEventModal = ({
           onSubmit={handleSubmit}
           onClose={onCancel}
           footerExtraButtons={
-            <ModalDangerButton type="button" borderless onClick={handleArchive}>
+            <ModalDangerButton onClick={handleArchive}>
               {t`Archive event`}
             </ModalDangerButton>
           }
diff --git a/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx
similarity index 94%
rename from frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.unit.spec.tsx
rename to frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx
index 961f8c0b65e..8bad3d3ed00 100644
--- a/frontend/src/metabase/timelines/collections/components/EditEventModal/EditEventModal.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/common/components/EditEventModal/EditEventModal.unit.spec.tsx
@@ -2,7 +2,6 @@ import React, { FormHTMLAttributes } from "react";
 import { render, screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import {
-  createMockCollection,
   createMockTimeline,
   createMockTimelineEvent,
 } from "metabase-types/api/mocks";
@@ -32,7 +31,6 @@ const getProps = (
 ): EditEventModalProps => ({
   event: createMockTimelineEvent(),
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   onSubmit: jest.fn(),
   onArchive: jest.fn(),
   onCancel: jest.fn(),
diff --git a/frontend/src/metabase/timelines/collections/components/EditEventModal/index.ts b/frontend/src/metabase/timelines/common/components/EditEventModal/index.ts
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/EditEventModal/index.ts
rename to frontend/src/metabase/timelines/common/components/EditEventModal/index.ts
diff --git a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.tsx b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx
similarity index 63%
rename from frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.tsx
rename to frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx
index 3be1a1a0d6e..ddf898d621e 100644
--- a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.tsx
@@ -2,24 +2,27 @@ import React, { useCallback, useMemo } from "react";
 import { t } from "ttag";
 import Form from "metabase/containers/Form";
 import forms from "metabase/entities/timelines/forms";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline } from "metabase-types/api";
-import { ModalDangerButton, ModalBody } from "./EditTimelineModal.styled";
+import { Timeline } from "metabase-types/api";
+import ModalBody from "../ModalBody";
+import ModalDangerButton from "../ModalDangerButton";
+import ModalHeader from "../ModalHeader";
 
 export interface EditTimelineModalProps {
   timeline: Timeline;
-  collection: Collection;
-  onSubmit: (values: Partial<Timeline>, collection: Collection) => void;
-  onArchive: (timeline: Timeline, collection: Collection) => void;
-  onCancel: () => void;
+  onSubmit: (values: Partial<Timeline>) => void;
+  onSubmitSuccess?: () => void;
+  onArchive: (timeline: Timeline) => void;
+  onArchiveSuccess?: () => void;
+  onCancel?: () => void;
   onClose?: () => void;
 }
 
 const EditTimelineModal = ({
   timeline,
-  collection,
   onSubmit,
+  onSubmitSuccess,
   onArchive,
+  onArchiveSuccess,
   onCancel,
   onClose,
 }: EditTimelineModalProps): JSX.Element => {
@@ -29,14 +32,16 @@ const EditTimelineModal = ({
 
   const handleSubmit = useCallback(
     async (values: Partial<Timeline>) => {
-      await onSubmit(values, collection);
+      await onSubmit(values);
+      onSubmitSuccess?.();
     },
-    [collection, onSubmit],
+    [onSubmit, onSubmitSuccess],
   );
 
   const handleArchive = useCallback(async () => {
-    await onArchive(timeline, collection);
-  }, [timeline, collection, onArchive]);
+    await onArchive(timeline);
+    onArchiveSuccess?.();
+  }, [timeline, onArchive, onArchiveSuccess]);
 
   return (
     <div>
@@ -49,7 +54,7 @@ const EditTimelineModal = ({
           onSubmit={handleSubmit}
           onClose={onCancel}
           footerExtraButtons={
-            <ModalDangerButton type="button" borderless onClick={handleArchive}>
+            <ModalDangerButton onClick={handleArchive}>
               {t`Archive timeline and all events`}
             </ModalDangerButton>
           }
diff --git a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
similarity index 87%
rename from frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
rename to frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
index 9ad1d233865..6a79e56ea25 100644
--- a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
+++ b/frontend/src/metabase/timelines/common/components/EditTimelineModal/EditTimelineModal.unit.spec.tsx
@@ -1,10 +1,7 @@
 import React, { FormHTMLAttributes } from "react";
 import { render, screen } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
-import {
-  createMockCollection,
-  createMockTimeline,
-} from "metabase-types/api/mocks";
+import { createMockTimeline } from "metabase-types/api/mocks";
 import EditTimelineModal, { EditTimelineModalProps } from "./EditTimelineModal";
 
 const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
@@ -30,7 +27,6 @@ const getProps = (
   opts?: Partial<EditTimelineModalProps>,
 ): EditTimelineModalProps => ({
   timeline: createMockTimeline(),
-  collection: createMockCollection(),
   onSubmit: jest.fn(),
   onArchive: jest.fn(),
   onCancel: jest.fn(),
diff --git a/frontend/src/metabase/timelines/collections/components/EditTimelineModal/index.ts b/frontend/src/metabase/timelines/common/components/EditTimelineModal/index.ts
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/EditTimelineModal/index.ts
rename to frontend/src/metabase/timelines/common/components/EditTimelineModal/index.ts
diff --git a/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.styled.tsx b/frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.styled.tsx
similarity index 61%
rename from frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.styled.tsx
rename to frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.styled.tsx
index 591d8b9a749..080d4ee70f5 100644
--- a/frontend/src/metabase/timelines/collections/components/NewEventModal/NewEventModal.styled.tsx
+++ b/frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.styled.tsx
@@ -1,5 +1,5 @@
 import styled from "@emotion/styled";
 
-export const ModalBody = styled.div`
+export const BodyRoot = styled.div`
   padding: 2rem;
 `;
diff --git a/frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.tsx b/frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.tsx
new file mode 100644
index 00000000000..f1c3999e058
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalBody/ModalBody.tsx
@@ -0,0 +1,12 @@
+import React, { ReactNode } from "react";
+import { BodyRoot } from "./ModalBody.styled";
+
+export interface ModalBodyProps {
+  children?: ReactNode;
+}
+
+const ModalBody = ({ children }: ModalBodyProps): JSX.Element => {
+  return <BodyRoot>{children}</BodyRoot>;
+};
+
+export default ModalBody;
diff --git a/frontend/src/metabase/timelines/common/components/ModalBody/index.ts b/frontend/src/metabase/timelines/common/components/ModalBody/index.ts
new file mode 100644
index 00000000000..a359bbd630d
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalBody/index.ts
@@ -0,0 +1 @@
+export { default } from "./ModalBody";
diff --git a/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.styled.tsx b/frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.styled.tsx
similarity index 73%
rename from frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.styled.tsx
rename to frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.styled.tsx
index 5d29609249e..3815f182af9 100644
--- a/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.styled.tsx
+++ b/frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.styled.tsx
@@ -2,11 +2,7 @@ import styled from "@emotion/styled";
 import { color } from "metabase/lib/colors";
 import Button from "metabase/core/components/Button/Button";
 
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
-
-export const ModalDangerButton = styled(Button)`
+export const DangerButton = styled(Button)`
   color: ${color("danger")};
   padding-left: 0;
   padding-right: 0;
diff --git a/frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.tsx b/frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.tsx
new file mode 100644
index 00000000000..819e8c8ebcd
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalDangerButton/ModalDangerButton.tsx
@@ -0,0 +1,20 @@
+import React, { MouseEvent, ReactNode } from "react";
+import { DangerButton } from "./ModalDangerButton.styled";
+
+export interface ModalDangerButtonProps {
+  children?: ReactNode;
+  onClick?: (event: MouseEvent) => void;
+}
+
+const ModalDangerButton = ({
+  children,
+  onClick,
+}: ModalDangerButtonProps): JSX.Element => {
+  return (
+    <DangerButton type="button" borderless onClick={onClick}>
+      {children}
+    </DangerButton>
+  );
+};
+
+export default ModalDangerButton;
diff --git a/frontend/src/metabase/timelines/common/components/ModalDangerButton/index.ts b/frontend/src/metabase/timelines/common/components/ModalDangerButton/index.ts
new file mode 100644
index 00000000000..1138f188221
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalDangerButton/index.ts
@@ -0,0 +1 @@
+export { default } from "./ModalDangerButton";
diff --git a/frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.styled.tsx b/frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.styled.tsx
new file mode 100644
index 00000000000..5cc8c122818
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.styled.tsx
@@ -0,0 +1,12 @@
+import styled from "@emotion/styled";
+
+export interface FooterProps {
+  hasPadding?: boolean;
+}
+
+export const FooterRoot = styled.div<FooterProps>`
+  display: flex;
+  gap: 1rem;
+  justify-content: flex-end;
+  padding: ${props => (props.hasPadding ? "2rem" : "0")} 2rem 2rem;
+`;
diff --git a/frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.tsx b/frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.tsx
new file mode 100644
index 00000000000..2c3b4a27ebf
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalFooter/ModalFooter.tsx
@@ -0,0 +1,16 @@
+import React, { ReactNode } from "react";
+import { FooterRoot } from "./ModalFooter.styled";
+
+export interface ModalFooterProps {
+  hasPadding?: boolean;
+  children?: ReactNode;
+}
+
+const ModalFooter = ({
+  hasPadding,
+  children,
+}: ModalFooterProps): JSX.Element => {
+  return <FooterRoot hasPadding={hasPadding}>{children}</FooterRoot>;
+};
+
+export default ModalFooter;
diff --git a/frontend/src/metabase/timelines/common/components/ModalFooter/index.ts b/frontend/src/metabase/timelines/common/components/ModalFooter/index.ts
new file mode 100644
index 00000000000..6a36137070f
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/ModalFooter/index.ts
@@ -0,0 +1 @@
+export { default } from "./ModalFooter";
diff --git a/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.styled.tsx b/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.styled.tsx
new file mode 100644
index 00000000000..f03ddd0b9ed
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.styled.tsx
@@ -0,0 +1,21 @@
+import styled from "@emotion/styled";
+
+export const ModalRoot = styled.div`
+  display: flex;
+  flex-direction: column;
+  min-height: 573px;
+  max-height: 90vh;
+`;
+
+export interface ModalBodyProps {
+  isTopAligned?: boolean;
+}
+
+export const ModalBody = styled.div<ModalBodyProps>`
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  margin: 1rem 0;
+  padding: 1rem 2rem;
+  overflow-y: auto;
+`;
diff --git a/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.tsx b/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.tsx
new file mode 100644
index 00000000000..9e5d35a3c06
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.tsx
@@ -0,0 +1,67 @@
+import React, { useCallback, useMemo, useState } from "react";
+import { t } from "ttag";
+import { getSortedTimelines } from "metabase/lib/timelines";
+import Button from "metabase/core/components/Button/Button";
+import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
+import ModalHeader from "../ModalHeader";
+import ModalFooter from "../ModalFooter";
+import TimelinePicker from "../TimelinePicker";
+import { ModalRoot, ModalBody } from "./MoveEventModal.styled";
+
+export interface MoveEventModalProps {
+  event: TimelineEvent;
+  timelines: Timeline[];
+  collection?: Collection;
+  onSubmit: (
+    event: TimelineEvent,
+    newTimeline?: Timeline,
+    oldTimeline?: Timeline,
+  ) => void;
+  onSubmitSuccess?: () => void;
+  onCancel?: () => void;
+  onClose?: () => void;
+}
+
+const MoveEventModal = ({
+  event,
+  timelines,
+  collection,
+  onSubmit,
+  onSubmitSuccess,
+  onCancel,
+  onClose,
+}: MoveEventModalProps): JSX.Element => {
+  const oldTimeline = timelines.find(t => t.id === event.timeline_id);
+  const [newTimeline, setNewTimeline] = useState(oldTimeline);
+  const isEnabled = newTimeline?.id !== oldTimeline?.id;
+
+  const sortedTimelines = useMemo(() => {
+    return getSortedTimelines(timelines, collection);
+  }, [timelines, collection]);
+
+  const handleSubmit = useCallback(async () => {
+    await onSubmit(event, newTimeline, oldTimeline);
+    onSubmitSuccess?.();
+  }, [event, newTimeline, oldTimeline, onSubmit, onSubmitSuccess]);
+
+  return (
+    <ModalRoot>
+      <ModalHeader title={t`Move ${event.name}`} onClose={onClose} />
+      <ModalBody>
+        <TimelinePicker
+          value={newTimeline}
+          options={sortedTimelines}
+          onChange={setNewTimeline}
+        />
+      </ModalBody>
+      <ModalFooter>
+        <Button onClick={onCancel}>{t`Cancel`}</Button>
+        <Button primary disabled={!isEnabled} onClick={handleSubmit}>
+          {t`Move`}
+        </Button>
+      </ModalFooter>
+    </ModalRoot>
+  );
+};
+
+export default MoveEventModal;
diff --git a/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.unit.spec.tsx
new file mode 100644
index 00000000000..a8f7e08399e
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/MoveEventModal/MoveEventModal.unit.spec.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import {
+  createMockTimeline,
+  createMockTimelineEvent,
+} from "metabase-types/api/mocks";
+import MoveEventModal, { MoveEventModalProps } from "./MoveEventModal";
+
+describe("MoveEventModal", () => {
+  it("should move an event to a different timeline", () => {
+    const event = createMockTimelineEvent({ timeline_id: 1 });
+    const oldTimeline = createMockTimeline({ id: 1, name: "Builds" });
+    const newTimeline = createMockTimeline({ id: 2, name: "Releases" });
+
+    const props = getProps({
+      event,
+      timelines: [oldTimeline, newTimeline],
+    });
+
+    render(<MoveEventModal {...props} />);
+    expect(screen.getByText("Move")).toBeDisabled();
+
+    userEvent.click(screen.getByText(newTimeline.name));
+    userEvent.click(screen.getByText("Move"));
+    expect(props.onSubmit).toHaveBeenLastCalledWith(
+      event,
+      newTimeline,
+      oldTimeline,
+    );
+  });
+});
+
+const getProps = (
+  opts?: Partial<MoveEventModalProps>,
+): MoveEventModalProps => ({
+  event: createMockTimelineEvent(),
+  timelines: [],
+  onSubmit: jest.fn(),
+  onCancel: jest.fn(),
+  onClose: jest.fn(),
+  ...opts,
+});
diff --git a/frontend/src/metabase/timelines/common/components/MoveEventModal/index.ts b/frontend/src/metabase/timelines/common/components/MoveEventModal/index.ts
new file mode 100644
index 00000000000..369d23ed2ee
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/MoveEventModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./MoveEventModal";
diff --git a/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx
similarity index 68%
rename from frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.tsx
rename to frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx
index b41bea50eb7..41c8fb91708 100644
--- a/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.tsx
+++ b/frontend/src/metabase/timelines/common/components/NewEventModal/NewEventModal.tsx
@@ -4,23 +4,38 @@ import { getDefaultTimezone } from "metabase/lib/time";
 import { getDefaultTimelineIcon } from "metabase/lib/timelines";
 import Form from "metabase/containers/Form";
 import forms from "metabase/entities/timeline-events/forms";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { Collection, Timeline, TimelineEvent } from "metabase-types/api";
-import { ModalBody } from "./NewEventModal.styled";
+import {
+  Collection,
+  Timeline,
+  TimelineEvent,
+  TimelineEventSource,
+} from "metabase-types/api";
+import ModalBody from "../ModalBody";
+import ModalHeader from "../ModalHeader";
 
 export interface NewEventModalProps {
-  cardId?: number;
   timelines?: Timeline[];
-  collection: Collection;
-  onSubmit: (values: Partial<TimelineEvent>, collection: Collection) => void;
+  collection?: Collection;
+  cardId?: number;
+  source: TimelineEventSource;
+  onSubmit: (
+    values: Partial<TimelineEvent>,
+    collection?: Collection,
+    timeline?: Timeline,
+  ) => void;
+  onSubmitSuccess?: () => void;
+  onCancel?: () => void;
   onClose?: () => void;
 }
 
 const NewEventModal = ({
-  cardId,
   timelines = [],
   collection,
+  cardId,
+  source,
   onSubmit,
+  onSubmitSuccess,
+  onCancel,
   onClose,
 }: NewEventModalProps): JSX.Element => {
   const availableTimelines = useMemo(() => {
@@ -39,18 +54,19 @@ const NewEventModal = ({
       timeline_id: defaultTimeline ? defaultTimeline.id : null,
       icon: hasOneTimeline ? defaultTimeline.icon : getDefaultTimelineIcon(),
       timezone: getDefaultTimezone(),
-      source: "question",
+      source,
       question_id: cardId,
       time_matters: false,
     };
-  }, [cardId, availableTimelines]);
+  }, [cardId, source, availableTimelines]);
 
   const handleSubmit = useCallback(
     async (values: Partial<TimelineEvent>) => {
-      await onSubmit(values, collection);
-      onClose?.();
+      const timeline = timelines.find(t => t.id === values.timeline_id);
+      await onSubmit(values, collection, timeline);
+      onSubmitSuccess?.();
     },
-    [collection, onSubmit, onClose],
+    [collection, timelines, onSubmit, onSubmitSuccess],
   );
 
   return (
@@ -62,7 +78,7 @@ const NewEventModal = ({
           initialValues={initialValues}
           isModal={true}
           onSubmit={handleSubmit}
-          onClose={onClose}
+          onClose={onCancel}
         />
       </ModalBody>
     </div>
diff --git a/frontend/src/metabase/timelines/collections/components/NewEventModal/index.ts b/frontend/src/metabase/timelines/common/components/NewEventModal/index.ts
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/NewEventModal/index.ts
rename to frontend/src/metabase/timelines/common/components/NewEventModal/index.ts
diff --git a/frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.tsx b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx
similarity index 85%
rename from frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.tsx
rename to frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx
index 2319f8dd973..a614269beb3 100644
--- a/frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.tsx
+++ b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.tsx
@@ -4,20 +4,22 @@ import Form from "metabase/containers/Form";
 import forms from "metabase/entities/timelines/forms";
 import { getDefaultTimelineIcon } from "metabase/lib/timelines";
 import { canonicalCollectionId } from "metabase/collections/utils";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
 import { Collection, Timeline } from "metabase-types/api";
-import { ModalBody } from "./NewTimelineModal.styled";
+import ModalBody from "../ModalBody";
+import ModalHeader from "../ModalHeader";
 
 export interface NewTimelineModalProps {
   collection: Collection;
   onSubmit: (values: Partial<Timeline>, collection: Collection) => void;
-  onCancel: () => void;
+  onSubmitSuccess?: () => void;
+  onCancel?: () => void;
   onClose?: () => void;
 }
 
 const NewTimelineModal = ({
   collection,
   onSubmit,
+  onSubmitSuccess,
   onCancel,
   onClose,
 }: NewTimelineModalProps): JSX.Element => {
@@ -32,8 +34,9 @@ const NewTimelineModal = ({
   const handleSubmit = useCallback(
     async (values: Partial<Timeline>) => {
       await onSubmit(values, collection);
+      onSubmitSuccess?.();
     },
-    [collection, onSubmit],
+    [collection, onSubmit, onSubmitSuccess],
   );
 
   return (
diff --git a/frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx b/frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx
rename to frontend/src/metabase/timelines/common/components/NewTimelineModal/NewTimelineModal.unit.spec.tsx
diff --git a/frontend/src/metabase/timelines/collections/components/NewTimelineModal/index.ts b/frontend/src/metabase/timelines/common/components/NewTimelineModal/index.ts
similarity index 100%
rename from frontend/src/metabase/timelines/collections/components/NewTimelineModal/index.ts
rename to frontend/src/metabase/timelines/common/components/NewTimelineModal/index.ts
diff --git a/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.stories.tsx b/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.stories.tsx
new file mode 100644
index 00000000000..1d53cba56f8
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.stories.tsx
@@ -0,0 +1,45 @@
+import React, { useState } from "react";
+import { ComponentStory } from "@storybook/react";
+import {
+  createMockCollection,
+  createMockTimeline,
+} from "metabase-types/api/mocks";
+import TimelinePicker from "./TimelinePicker";
+import { Timeline } from "metabase-types/api";
+
+export default {
+  title: "Timelines/TimelinePicker",
+  component: TimelinePicker,
+};
+
+const Template: ComponentStory<typeof TimelinePicker> = args => {
+  const [value, setValue] = useState<Timeline>();
+  return <TimelinePicker {...args} value={value} onChange={setValue} />;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+  options: [
+    createMockTimeline({
+      id: 1,
+      name: "Product communications",
+      collection: createMockCollection({
+        name: "Our analytics",
+      }),
+    }),
+    createMockTimeline({
+      id: 2,
+      name: "Releases",
+      collection: createMockCollection({
+        name: "Our analytics",
+      }),
+    }),
+    createMockTimeline({
+      id: 3,
+      name: "Our analytics events",
+      collection: createMockCollection({
+        name: "Our analytics",
+      }),
+    }),
+  ],
+};
diff --git a/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.styled.tsx b/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.styled.tsx
new file mode 100644
index 00000000000..45dc646c88e
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.styled.tsx
@@ -0,0 +1,89 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+import Icon from "metabase/components/Icon";
+import { css } from "@emotion/react";
+
+export const ListRoot = styled.div`
+  display: block;
+`;
+
+export const CardBody = styled.div`
+  flex: 1 1 auto;
+  margin: 0 1rem;
+  min-width: 0;
+`;
+
+export const CardTitle = styled.div`
+  color: ${color("text-dark")};
+  font-size: 1rem;
+  font-weight: bold;
+  margin-bottom: 0.125rem;
+  word-wrap: break-word;
+`;
+
+export const CardDescription = styled.div`
+  color: ${color("text-medium")};
+  font-size: 0.75rem;
+  word-wrap: break-word;
+`;
+
+export const CardIcon = styled(Icon)`
+  color: ${color("text-dark")};
+  width: 1rem;
+  height: 1rem;
+`;
+
+export const CardIconContainer = styled.div`
+  display: flex;
+  flex: 0 0 auto;
+  justify-content: center;
+  align-items: center;
+  width: 2rem;
+  height: 2rem;
+  border: 1px solid ${color("border")};
+  border-radius: 1rem;
+`;
+
+export const CardAside = styled.div`
+  flex: 0 0 auto;
+  color: ${color("text-dark")};
+  font-size: 0.75rem;
+`;
+
+export interface CardProps {
+  isSelected?: boolean;
+}
+
+const selectedStyles = css`
+  background-color: ${color("brand")};
+
+  ${CardTitle}, ${CardDescription}, ${CardAside} {
+    color: ${color("white")};
+  }
+
+  ${CardIcon} {
+    color: ${color("brand")};
+  }
+
+  ${CardIconContainer} {
+    border-color: ${color("white")};
+    background-color: ${color("white")};
+  }
+`;
+
+export const CardRoot = styled.div<CardProps>`
+  display: flex;
+  align-items: center;
+  padding: 1rem;
+  border-radius: 0.25rem;
+  cursor: pointer;
+  ${props => props.isSelected && selectedStyles}
+
+  &:hover {
+    ${selectedStyles}
+  }
+
+  &:not(:last-child) {
+    margin-bottom: 0.5rem;
+  }
+`;
diff --git a/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.tsx b/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.tsx
new file mode 100644
index 00000000000..cba16bbb4fd
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/TimelinePicker/TimelinePicker.tsx
@@ -0,0 +1,74 @@
+import React, { useCallback } from "react";
+import { msgid, ngettext } from "ttag";
+import { getEventCount } from "metabase/lib/timelines";
+import { Timeline } from "metabase-types/api";
+import {
+  CardAside,
+  CardBody,
+  CardDescription,
+  CardIcon,
+  CardIconContainer,
+  CardRoot,
+  CardTitle,
+  ListRoot,
+} from "./TimelinePicker.styled";
+
+export interface TimelinePickerProps {
+  value?: Timeline;
+  options: Timeline[];
+  onChange?: (value: Timeline) => void;
+}
+
+const TimelinePicker = ({ value, options, onChange }: TimelinePickerProps) => {
+  return (
+    <ListRoot>
+      {options.map(option => (
+        <TimelineCard
+          key={option.id}
+          timeline={option}
+          isSelected={option.id === value?.id}
+          onChange={onChange}
+        />
+      ))}
+    </ListRoot>
+  );
+};
+
+interface TimelineCardProps {
+  timeline: Timeline;
+  isSelected: boolean;
+  onChange?: (value: Timeline) => void;
+}
+
+const TimelineCard = ({
+  timeline,
+  isSelected,
+  onChange,
+}: TimelineCardProps): JSX.Element => {
+  const eventCount = getEventCount(timeline);
+
+  const handleClick = useCallback(() => {
+    onChange?.(timeline);
+  }, [timeline, onChange]);
+
+  return (
+    <CardRoot key={timeline.id} isSelected={isSelected} onClick={handleClick}>
+      <CardIconContainer>
+        <CardIcon name={timeline.icon} />
+      </CardIconContainer>
+      <CardBody>
+        <CardTitle>{timeline.name}</CardTitle>
+        <CardDescription>{timeline.description}</CardDescription>
+      </CardBody>
+      <CardAside>
+        {ngettext(
+          msgid`${eventCount} event`,
+          `${eventCount} events`,
+          eventCount,
+        )}
+      </CardAside>
+    </CardRoot>
+  );
+};
+
+export default TimelinePicker;
diff --git a/frontend/src/metabase/timelines/common/components/TimelinePicker/index.ts b/frontend/src/metabase/timelines/common/components/TimelinePicker/index.ts
new file mode 100644
index 00000000000..0878b8e1497
--- /dev/null
+++ b/frontend/src/metabase/timelines/common/components/TimelinePicker/index.ts
@@ -0,0 +1 @@
+export { default } from "./TimelinePicker";
diff --git a/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.tsx
deleted file mode 100644
index 6b74ca85c02..00000000000
--- a/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { useCallback, useMemo } from "react";
-import { t } from "ttag";
-import Form from "metabase/containers/Form";
-import forms from "metabase/entities/timeline-events/forms";
-import { TimelineEvent } from "metabase-types/api";
-import ModalHeader from "metabase/timelines/common/components/ModalHeader";
-import { ModalBody, ModalDangerButton } from "./EditEventModal.styled";
-
-export interface EditEventModalProps {
-  event: TimelineEvent;
-  onSubmit: (event: TimelineEvent) => void;
-  onArchive: (event: TimelineEvent) => void;
-  onClose?: () => void;
-}
-
-const EditEventModal = ({
-  event,
-  onSubmit,
-  onArchive,
-  onClose,
-}: EditEventModalProps): JSX.Element => {
-  const form = useMemo(() => forms.details(), []);
-
-  const handleSubmit = useCallback(
-    async (event: TimelineEvent) => {
-      await onSubmit(event);
-      onClose?.();
-    },
-    [onSubmit, onClose],
-  );
-
-  const handleArchive = useCallback(async () => {
-    await onArchive(event);
-    onClose?.();
-  }, [event, onArchive, onClose]);
-
-  return (
-    <div>
-      <ModalHeader title={t`Edit event`} onClose={onClose} />
-      <ModalBody>
-        <Form
-          form={form}
-          initialValues={event}
-          isModal={true}
-          onSubmit={handleSubmit}
-          onClose={onClose}
-          footerExtraButtons={
-            <ModalDangerButton type="button" borderless onClick={handleArchive}>
-              {t`Archive event`}
-            </ModalDangerButton>
-          }
-        />
-      </ModalBody>
-    </div>
-  );
-};
-
-export default EditEventModal;
diff --git a/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.unit.spec.tsx
deleted file mode 100644
index 70780fdb895..00000000000
--- a/frontend/src/metabase/timelines/questions/components/EditEventModal/EditEventModal.unit.spec.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { FormHTMLAttributes } from "react";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { createMockTimelineEvent } from "metabase-types/api/mocks";
-import EditEventModal, { EditEventModalProps } from "./EditEventModal";
-
-const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
-  <form {...props}>
-    <button>Update</button>
-  </form>
-);
-
-jest.mock("metabase/containers/Form", () => FormMock);
-
-describe("EditEventModal", () => {
-  it("should submit modal", () => {
-    const props = getProps();
-
-    render(<EditEventModal {...props} />);
-    userEvent.click(screen.getByText("Update"));
-
-    expect(props.onSubmit).toHaveBeenCalled();
-  });
-});
-
-const getProps = (
-  opts?: Partial<EditEventModalProps>,
-): EditEventModalProps => ({
-  event: createMockTimelineEvent(),
-  onSubmit: jest.fn(),
-  onArchive: jest.fn(),
-  onClose: jest.fn(),
-  ...opts,
-});
diff --git a/frontend/src/metabase/timelines/questions/components/EditEventModal/index.ts b/frontend/src/metabase/timelines/questions/components/EditEventModal/index.ts
deleted file mode 100644
index 993e930fb2e..00000000000
--- a/frontend/src/metabase/timelines/questions/components/EditEventModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./EditEventModal";
diff --git a/frontend/src/metabase/timelines/questions/components/EventCard/EventCard.tsx b/frontend/src/metabase/timelines/questions/components/EventCard/EventCard.tsx
index b38752870b4..21665a5b683 100644
--- a/frontend/src/metabase/timelines/questions/components/EventCard/EventCard.tsx
+++ b/frontend/src/metabase/timelines/questions/components/EventCard/EventCard.tsx
@@ -23,6 +23,7 @@ export interface EventCardProps {
   timeline: Timeline;
   isSelected?: boolean;
   onEdit?: (event: TimelineEvent) => void;
+  onMove?: (event: TimelineEvent) => void;
   onArchive?: (event: TimelineEvent) => void;
   onToggle?: (event: TimelineEvent, isSelected: boolean) => void;
 }
@@ -32,11 +33,12 @@ const EventCard = ({
   timeline,
   isSelected,
   onEdit,
+  onMove,
   onArchive,
   onToggle,
 }: EventCardProps): JSX.Element => {
   const selectedRef = useScrollOnMount();
-  const menuItems = getMenuItems(event, timeline, onEdit, onArchive);
+  const menuItems = getMenuItems(event, timeline, onEdit, onMove, onArchive);
   const dateMessage = getDateMessage(event);
   const creatorMessage = getCreatorMessage(event);
 
@@ -78,6 +80,7 @@ const getMenuItems = (
   event: TimelineEvent,
   timeline: Timeline,
   onEdit?: (event: TimelineEvent) => void,
+  onMove?: (event: TimelineEvent) => void,
   onArchive?: (event: TimelineEvent) => void,
 ) => {
   if (!timeline.collection?.can_write) {
@@ -89,6 +92,10 @@ const getMenuItems = (
       title: t`Edit event`,
       action: () => onEdit?.(event),
     },
+    {
+      title: t`Move event`,
+      action: () => onMove?.(event),
+    },
     {
       title: t`Archive event`,
       action: () => onArchive?.(event),
diff --git a/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.styled.tsx b/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.styled.tsx
deleted file mode 100644
index 591d8b9a749..00000000000
--- a/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.styled.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import styled from "@emotion/styled";
-
-export const ModalBody = styled.div`
-  padding: 2rem;
-`;
diff --git a/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.unit.spec.tsx b/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.unit.spec.tsx
deleted file mode 100644
index 56524ff81ae..00000000000
--- a/frontend/src/metabase/timelines/questions/components/NewEventModal/NewEventModal.unit.spec.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { FormHTMLAttributes } from "react";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { createMockCollection } from "metabase-types/api/mocks";
-import NewEventModal, { NewEventModalProps } from "./NewEventModal";
-
-const FormMock = (props: FormHTMLAttributes<HTMLFormElement>) => (
-  <form {...props}>
-    <button>Create</button>
-  </form>
-);
-
-jest.mock("metabase/containers/Form", () => FormMock);
-
-describe("NewEventModal", () => {
-  it("should submit modal", () => {
-    const props = getProps();
-
-    render(<NewEventModal {...props} />);
-    userEvent.click(screen.getByText("Create"));
-
-    expect(props.onSubmit).toHaveBeenCalled();
-  });
-});
-
-const getProps = (opts?: Partial<NewEventModalProps>): NewEventModalProps => ({
-  collection: createMockCollection(),
-  onSubmit: jest.fn(),
-  onClose: jest.fn(),
-  ...opts,
-});
diff --git a/frontend/src/metabase/timelines/questions/components/NewEventModal/index.ts b/frontend/src/metabase/timelines/questions/components/NewEventModal/index.ts
deleted file mode 100644
index 58a19000dc2..00000000000
--- a/frontend/src/metabase/timelines/questions/components/NewEventModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./NewEventModal";
diff --git a/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx b/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx
index 40e5c29fb1d..247321ccf24 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelineCard/TimelineCard.tsx
@@ -26,6 +26,7 @@ export interface TimelineCardProps {
   isVisible?: boolean;
   selectedEventIds?: number[];
   onEditEvent?: (event: TimelineEvent) => void;
+  onMoveEvent?: (event: TimelineEvent) => void;
   onArchiveEvent?: (event: TimelineEvent) => void;
   onToggleEvent?: (event: TimelineEvent, isSelected: boolean) => void;
   onToggleTimeline?: (timeline: Timeline, isVisible: boolean) => void;
@@ -37,6 +38,7 @@ const TimelineCard = ({
   isVisible,
   selectedEventIds = [],
   onEditEvent,
+  onMoveEvent,
   onArchiveEvent,
   onToggleEvent,
   onToggleTimeline,
@@ -90,6 +92,7 @@ const TimelineCard = ({
               timeline={timeline}
               isSelected={selectedEventIds.includes(event.id)}
               onEdit={onEditEvent}
+              onMove={onMoveEvent}
               onArchive={onArchiveEvent}
               onToggle={onToggleEvent}
             />
diff --git a/frontend/src/metabase/timelines/questions/components/TimelineList/TimelineList.tsx b/frontend/src/metabase/timelines/questions/components/TimelineList/TimelineList.tsx
index 588078b3b00..7ff2ff9e3e8 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelineList/TimelineList.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelineList/TimelineList.tsx
@@ -7,6 +7,7 @@ export interface TimelineListProps {
   visibleTimelineIds?: number[];
   selectedEventIds?: number[];
   onEditEvent?: (event: TimelineEvent) => void;
+  onMoveEvent?: (event: TimelineEvent) => void;
   onArchiveEvent?: (event: TimelineEvent) => void;
   onToggleEvent?: (event: TimelineEvent, isSelected: boolean) => void;
   onToggleTimeline?: (timeline: Timeline, isVisible: boolean) => void;
@@ -17,6 +18,7 @@ const TimelineList = ({
   visibleTimelineIds = [],
   selectedEventIds = [],
   onEditEvent,
+  onMoveEvent,
   onArchiveEvent,
   onToggleEvent,
   onToggleTimeline,
@@ -32,6 +34,7 @@ const TimelineList = ({
           selectedEventIds={selectedEventIds}
           onToggleTimeline={onToggleTimeline}
           onEditEvent={onEditEvent}
+          onMoveEvent={onMoveEvent}
           onToggleEvent={onToggleEvent}
           onArchiveEvent={onArchiveEvent}
         />
diff --git a/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx b/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx
index cafeb38927f..cb80623be7f 100644
--- a/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx
+++ b/frontend/src/metabase/timelines/questions/components/TimelinePanel/TimelinePanel.tsx
@@ -13,6 +13,7 @@ export interface TimelinePanelProps {
   selectedEventIds?: number[];
   onNewEvent?: () => void;
   onEditEvent?: (event: TimelineEvent) => void;
+  onMoveEvent?: (event: TimelineEvent) => void;
   onArchiveEvent?: (event: TimelineEvent) => void;
   onToggleEvent?: (event: TimelineEvent, isSelected: boolean) => void;
   onToggleTimeline?: (timeline: Timeline, isVisible: boolean) => void;
@@ -25,6 +26,7 @@ const TimelinePanel = ({
   selectedEventIds,
   onNewEvent,
   onEditEvent,
+  onMoveEvent,
   onArchiveEvent,
   onToggleEvent,
   onToggleTimeline,
@@ -46,6 +48,7 @@ const TimelinePanel = ({
           selectedEventIds={selectedEventIds}
           onToggleTimeline={onToggleTimeline}
           onEditEvent={onEditEvent}
+          onMoveEvent={onMoveEvent}
           onToggleEvent={onToggleEvent}
           onArchiveEvent={onArchiveEvent}
         />
diff --git a/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx b/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx
index 67327b68842..dff7532639c 100644
--- a/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx
+++ b/frontend/src/metabase/timelines/questions/containers/EditEventModal/EditEventModal.tsx
@@ -3,12 +3,13 @@ import { t } from "ttag";
 import _ from "underscore";
 import TimelineEvents from "metabase/entities/timeline-events";
 import { addUndo } from "metabase/redux/undo";
-import { TimelineEvent } from "metabase-types/api";
+import EditEventModal from "metabase/timelines/common/components/EditEventModal";
+import { Timeline, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import EditEventModal from "../../components/EditEventModal";
 
-export interface EditEventModalProps {
+interface EditEventModalProps {
   eventId: number;
+  onClose?: () => void;
 }
 
 const timelineEventProps = {
@@ -16,8 +17,14 @@ const timelineEventProps = {
   entityAlias: "event",
 };
 
+const mapStateToProps = (state: State, { onClose }: EditEventModalProps) => ({
+  onSubmitSuccess: onClose,
+  onArchiveSuccess: onClose,
+  onCancel: onClose,
+});
+
 const mapDispatchToProps = (dispatch: any) => ({
-  onSubmit: async (event: TimelineEvent) => {
+  onSubmit: async (event: TimelineEvent, timeline?: Timeline) => {
     await dispatch(TimelineEvents.actions.update(event));
     dispatch(addUndo({ message: t`Updated event` }));
   },
@@ -28,5 +35,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 
 export default _.compose(
   TimelineEvents.load(timelineEventProps),
-  connect(null, mapDispatchToProps),
+  connect(mapStateToProps, mapDispatchToProps),
 )(EditEventModal);
diff --git a/frontend/src/metabase/timelines/questions/containers/MoveEventModal/MoveEventModal.tsx b/frontend/src/metabase/timelines/questions/containers/MoveEventModal/MoveEventModal.tsx
new file mode 100644
index 00000000000..796cffd8bc9
--- /dev/null
+++ b/frontend/src/metabase/timelines/questions/containers/MoveEventModal/MoveEventModal.tsx
@@ -0,0 +1,53 @@
+import { connect } from "react-redux";
+import _ from "underscore";
+import Collections, { ROOT_COLLECTION } from "metabase/entities/collections";
+import Timelines from "metabase/entities/timelines";
+import TimelineEvents from "metabase/entities/timeline-events";
+import MoveEventModal from "metabase/timelines/common/components/MoveEventModal";
+import { Timeline, TimelineEvent } from "metabase-types/api";
+import { State } from "metabase-types/store";
+
+interface MoveEventModalProps {
+  eventId: number;
+  collectionId?: number;
+  onClose?: () => void;
+}
+
+const timelinesProps = {
+  query: { include: "events" },
+};
+
+const timelineEventProps = {
+  id: (state: State, props: MoveEventModalProps) => props.eventId,
+  entityAlias: "event",
+};
+
+const collectionProps = {
+  id: (state: State, props: MoveEventModalProps) => {
+    return props.collectionId ?? ROOT_COLLECTION.id;
+  },
+};
+
+const mapStateToProps = (state: State, { onClose }: MoveEventModalProps) => ({
+  onSubmitSuccess: onClose,
+  onCancel: onClose,
+});
+
+const mapDispatchToProps = (dispatch: any) => ({
+  onSubmit: async (
+    event: TimelineEvent,
+    newTimeline: Timeline,
+    oldTimeline: Timeline,
+    onClose?: () => void,
+  ) => {
+    await dispatch(TimelineEvents.actions.setTimeline(event, newTimeline));
+    onClose?.();
+  },
+});
+
+export default _.compose(
+  Timelines.loadList(timelinesProps),
+  TimelineEvents.load(timelineEventProps),
+  Collections.load(collectionProps),
+  connect(mapStateToProps, mapDispatchToProps),
+)(MoveEventModal);
diff --git a/frontend/src/metabase/timelines/questions/containers/MoveEventModal/index.ts b/frontend/src/metabase/timelines/questions/containers/MoveEventModal/index.ts
new file mode 100644
index 00000000000..369d23ed2ee
--- /dev/null
+++ b/frontend/src/metabase/timelines/questions/containers/MoveEventModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./MoveEventModal";
diff --git a/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx b/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx
index 1ec656a2631..68049348b36 100644
--- a/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx
+++ b/frontend/src/metabase/timelines/questions/containers/NewEventModal/NewEventModal.tsx
@@ -5,13 +5,14 @@ import Collections, { ROOT_COLLECTION } from "metabase/entities/collections";
 import Timelines from "metabase/entities/timelines";
 import TimelineEvents from "metabase/entities/timeline-events";
 import { addUndo } from "metabase/redux/undo";
+import NewEventModal from "metabase/timelines/common/components/NewEventModal";
 import { Collection, TimelineEvent } from "metabase-types/api";
 import { State } from "metabase-types/store";
-import NewEventModal from "../../components/NewEventModal";
 
-interface TimelinePanelProps {
+interface NewEventModalProps {
   cardId?: number;
   collectionId?: number;
+  onClose?: () => void;
 }
 
 const timelineProps = {
@@ -19,11 +20,17 @@ const timelineProps = {
 };
 
 const collectionProps = {
-  id: (state: State, props: TimelinePanelProps) => {
+  id: (state: State, props: NewEventModalProps) => {
     return props.collectionId ?? ROOT_COLLECTION.id;
   },
 };
 
+const mapStateToProps = (state: State, { onClose }: NewEventModalProps) => ({
+  source: "question",
+  onSubmitSuccess: onClose,
+  onCancel: onClose,
+});
+
 const mapDispatchToProps = (dispatch: any) => ({
   onSubmit: async (values: Partial<TimelineEvent>, collection: Collection) => {
     if (values.timeline_id) {
@@ -39,5 +46,5 @@ const mapDispatchToProps = (dispatch: any) => ({
 export default _.compose(
   Timelines.loadList(timelineProps),
   Collections.load(collectionProps),
-  connect(null, mapDispatchToProps),
+  connect(mapStateToProps, mapDispatchToProps),
 )(NewEventModal);
diff --git a/frontend/test/metabase-visual/collections/timelines.cy.spec.js b/frontend/test/metabase-visual/collections/timelines.cy.spec.js
index b2ee8a57362..c29c59776f7 100644
--- a/frontend/test/metabase-visual/collections/timelines.cy.spec.js
+++ b/frontend/test/metabase-visual/collections/timelines.cy.spec.js
@@ -15,7 +15,7 @@ describe("timelines", () => {
   it("should display empty state", () => {
     cy.visit("/collection/root/timelines");
 
-    cy.findByText("Our analytics events");
+    cy.findByText("Our analytics events").should("be.visible");
     cy.percySnapshot();
   });
 
@@ -23,7 +23,7 @@ describe("timelines", () => {
     cy.createTimelineWithEvents({ events: EVENTS }).then(({ timeline }) => {
       cy.visit(`/collection/root/timelines/${timeline.id}`);
 
-      cy.findByText("Timeline");
+      cy.findByText("Timeline").should("be.visible");
       cy.percySnapshot();
     });
   });
diff --git a/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js b/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js
index 61d6cb07813..9f87918b35c 100644
--- a/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js
+++ b/frontend/test/metabase/scenarios/collections/timelines.cy.spec.js
@@ -179,6 +179,55 @@ describe("scenarios > collections > timelines", () => {
       cy.findByText("RC2").should("be.visible");
     });
 
+    it("should move an event", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }],
+      });
+      cy.createTimelineWithEvents({
+        timeline: { name: "Metrics" },
+        events: [{ name: "RC2" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      cy.findByText("Metrics").click();
+      openMenu("RC2");
+      cy.findByText("Move event").click();
+      cy.findByText("Releases").click();
+      getModal().within(() => cy.button("Move").click());
+      cy.wait("@updateEvent");
+      cy.findByText("RC2").should("not.exist");
+
+      cy.icon("chevronleft").click();
+      cy.findByText("Releases").click();
+      cy.findByText("RC1");
+      cy.findByText("RC2");
+    });
+
+    it("should move an event and undo", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [{ name: "RC1" }],
+      });
+      cy.createTimelineWithEvents({
+        timeline: { name: "Metrics" },
+        events: [{ name: "RC2" }],
+      });
+
+      cy.visit("/collection/root/timelines");
+      cy.findByText("Metrics").click();
+      openMenu("RC2");
+      cy.findByText("Move event").click();
+      cy.findByText("Releases").click();
+      getModal().within(() => cy.button("Move").click());
+      cy.wait("@updateEvent");
+      cy.findByText("RC2").should("not.exist");
+
+      cy.findByText("Undo").click();
+      cy.wait("@updateEvent");
+      cy.findByText("RC2").should("be.visible");
+    });
+
     it("should archive an event when editing this event", () => {
       cy.createTimelineWithEvents({
         timeline: { name: "Releases" },
diff --git a/frontend/test/metabase/scenarios/question/timelines.cy.spec.js b/frontend/test/metabase/scenarios/question/timelines.cy.spec.js
index 96082e5873a..dec9b690cdf 100644
--- a/frontend/test/metabase/scenarios/question/timelines.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/timelines.cy.spec.js
@@ -16,18 +16,18 @@ describe("scenarios > collections > timelines", () => {
     it("should create the first event and timeline", () => {
       visitQuestion(3);
       cy.wait("@getCollection");
-      cy.findByTextEnsureVisible("Visualization");
+      cy.findByText("Visualization").should("be.visible");
 
       cy.icon("calendar").click();
-      cy.findByTextEnsureVisible("Add an event").click();
+      cy.findByText("Add an event").click();
 
       cy.findByLabelText("Event name").type("RC1");
       cy.findByLabelText("Date").type("10/20/2018");
       cy.button("Create").click();
       cy.wait("@createEvent");
 
-      cy.findByTextEnsureVisible("Our analytics events");
-      cy.findByText("RC1");
+      cy.findByText("Our analytics events").should("be.visible");
+      cy.findByText("RC1").should("be.visible");
     });
 
     it("should create an event within the default timeline", () => {
@@ -38,19 +38,44 @@ describe("scenarios > collections > timelines", () => {
 
       visitQuestion(3);
       cy.wait("@getCollection");
-      cy.findByTextEnsureVisible("Visualization");
+      cy.findByText("Visualization").should("be.visible");
 
       cy.icon("calendar").click();
-      cy.findByTextEnsureVisible("Add an event").click();
+      cy.findByText("Add an event").click();
 
       cy.findByLabelText("Event name").type("RC2");
       cy.findByLabelText("Date").type("10/30/2018");
       cy.button("Create").click();
       cy.wait("@createEvent");
 
-      cy.findByTextEnsureVisible("Releases");
-      cy.findByText("RC1");
-      cy.findByText("RC2");
+      cy.findByText("Releases").should("be.visible");
+      cy.findByText("RC1").should("be.visible");
+      cy.findByText("RC2").should("be.visible");
+    });
+
+    it("should display all events in data view", () => {
+      cy.createTimelineWithEvents({
+        timeline: { name: "Releases" },
+        events: [
+          { name: "v1", timestamp: "2015-01-01T00:00:00Z" },
+          { name: "v2", timestamp: "2017-01-01T00:00:00Z" },
+          { name: "v3", timestamp: "2020-01-01T00:00:00Z" },
+        ],
+      });
+
+      visitQuestion(3);
+      cy.wait("@getCollection");
+      cy.findByText("Visualization").should("be.visible");
+
+      cy.findByLabelText("calendar icon").click();
+      cy.findByText("v1").should("not.exist");
+      cy.findByText("v2").should("be.visible");
+      cy.findByText("v3").should("be.visible");
+
+      cy.findByLabelText("table2 icon").click();
+      cy.findByText("v1").should("be.visible");
+      cy.findByText("v2").should("be.visible");
+      cy.findByText("v3").should("be.visible");
     });
 
     it("should edit an event", () => {
@@ -61,12 +86,12 @@ describe("scenarios > collections > timelines", () => {
 
       visitQuestion(3);
       cy.wait("@getCollection");
-      cy.findByTextEnsureVisible("Visualization");
+      cy.findByText("Visualization").should("be.visible");
 
       cy.icon("calendar").click();
-      cy.findByText("Releases");
+      cy.findByText("Releases").should("be.visible");
       sidebar().within(() => cy.icon("ellipsis").click());
-      cy.findByTextEnsureVisible("Edit event").click();
+      cy.findByText("Edit event").click();
 
       cy.findByLabelText("Event name")
         .clear()
@@ -74,33 +99,33 @@ describe("scenarios > collections > timelines", () => {
       cy.findByText("Update").click();
       cy.wait("@updateEvent");
 
-      cy.findByTextEnsureVisible("Releases");
-      cy.findByText("RC2");
+      cy.findByText("Releases").should("be.visible");
+      cy.findByText("RC2").should("be.visible");
     });
 
-    it("should display all events in data view", () => {
+    it("should move an event", () => {
+      cy.createTimeline({
+        name: "Releases",
+      });
       cy.createTimelineWithEvents({
-        timeline: { name: "Releases" },
-        events: [
-          { name: "v1", timestamp: "2015-01-01T00:00:00Z" },
-          { name: "v2", timestamp: "2017-01-01T00:00:00Z" },
-          { name: "v3", timestamp: "2020-01-01T00:00:00Z" },
-        ],
+        timeline: { name: "Builds" },
+        events: [{ name: "RC2", timestamp: "2018-10-20T00:00:00Z" }],
       });
 
       visitQuestion(3);
       cy.wait("@getCollection");
-      cy.findByTextEnsureVisible("Visualization");
+      cy.findByText("Visualization").should("be.visible");
 
-      cy.findByLabelText("calendar icon").click();
-      cy.findByText("v1").should("not.exist");
-      cy.findByText("v2").should("be.visible");
-      cy.findByText("v3").should("be.visible");
+      cy.icon("calendar").click();
+      cy.findByText("Builds").should("be.visible");
+      sidebar().within(() => cy.icon("ellipsis").click());
+      cy.findByText("Move event").click();
+      cy.findByText("Releases").click();
+      cy.button("Move").click();
+      cy.wait("@updateEvent");
 
-      cy.findByLabelText("table2 icon").click();
-      cy.findByText("v1").should("be.visible");
-      cy.findByText("v2").should("be.visible");
-      cy.findByText("v3").should("be.visible");
+      cy.findByText("Builds").should("not.exist");
+      cy.findByText("Releases").should("be.visible");
     });
 
     it("should archive and unarchive an event", () => {
@@ -111,18 +136,18 @@ describe("scenarios > collections > timelines", () => {
 
       visitQuestion(3);
       cy.wait("@getCollection");
-      cy.findByTextEnsureVisible("Visualization");
+      cy.findByText("Visualization").should("be.visible");
 
       cy.icon("calendar").click();
-      cy.findByText("Releases");
+      cy.findByText("Releases").should("be.visible");
       sidebar().within(() => cy.icon("ellipsis").click());
-      cy.findByTextEnsureVisible("Archive event").click();
+      cy.findByText("Archive event").click();
       cy.wait("@updateEvent");
       cy.findByText("RC1").should("not.exist");
 
       cy.findByText("Undo").click();
       cy.wait("@updateEvent");
-      cy.findByText("RC1");
+      cy.findByText("RC1").should("be.visible");
     });
 
     it("should support markdown in event description", () => {
@@ -150,10 +175,10 @@ describe("scenarios > collections > timelines", () => {
     it("should not allow creating default timelines", () => {
       cy.signIn("readonly");
       visitQuestion(3);
-      cy.findByTextEnsureVisible("Created At");
+      cy.findByText("Created At").should("be.visible");
 
       cy.icon("calendar").click();
-      cy.findByTextEnsureVisible(/Events in Metabase/);
+      cy.findByText(/Events in Metabase/);
       cy.findByText("Add an event").should("not.exist");
     });
 
@@ -166,10 +191,10 @@ describe("scenarios > collections > timelines", () => {
       cy.signOut();
       cy.signIn("readonly");
       visitQuestion(3);
-      cy.findByTextEnsureVisible("Created At");
+      cy.findByText("Created At").should("be.visible");
 
       cy.icon("calendar").click();
-      cy.findByTextEnsureVisible("Releases");
+      cy.findByText("Releases").should("be.visible");
       cy.findByText("Add an event").should("not.exist");
       sidebar().within(() => cy.icon("ellipsis").should("not.exist"));
     });
-- 
GitLab