Skip to content

Commit

Permalink
Remix projects context menu (#4893)
Browse files Browse the repository at this point in the history
* context menu

* remove unused

* use radix

* update imports

* use button style

* remove old commented out

* fix colors
  • Loading branch information
ruggi authored Feb 14, 2024
1 parent bfd230f commit df30077
Show file tree
Hide file tree
Showing 9 changed files with 852 additions and 55 deletions.
163 changes: 163 additions & 0 deletions utopia-remix/app/components/projectActionContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'

import { useFetcher } from '@remix-run/react'
import React from 'react'
import { Category } from '../routes/projects'
import { ProjectWithoutContent } from '../types'
import { assertNever } from '../util/assertNever'
import { projectEditorLink } from '../util/links'
import { contextMenuItem } from '../styles/contextMenuItem.css'
import { colors } from '../styles/sprinkles.css'

type ContextMenuEntry =
| {
text: string
onClick: (project: ProjectWithoutContent) => void
}
| 'separator'

export const ProjectContextMenu = React.memo(
({
selectedCategory,
project,
}: {
selectedCategory: Category
project: ProjectWithoutContent
}) => {
const fetcher = useFetcher()

const deleteProject = React.useCallback(
(projectId: string) => {
fetcher.submit({}, { method: 'POST', action: `/internal/projects/${projectId}/delete` })
},
[fetcher],
)

const destroyProject = React.useCallback(
(projectId: string) => {
const ok = window.confirm('Are you sure? The project contents will be deleted permanently.')
if (ok) {
fetcher.submit({}, { method: 'POST', action: `/internal/projects/${projectId}/destroy` })
}
},
[fetcher],
)

const restoreProject = React.useCallback(
(projectId: string) => {
fetcher.submit({}, { method: 'POST', action: `/internal/projects/${projectId}/restore` })
},
[fetcher],
)

const renameProject = React.useCallback(
(projectId: string, newTitle: string) => {
fetcher.submit(
{ title: newTitle },
{ method: 'POST', action: `/internal/projects/${projectId}/rename` },
)
},
[fetcher],
)

const menuEntries = React.useMemo((): ContextMenuEntry[] => {
switch (selectedCategory) {
case 'allProjects':
return [
{
text: 'Open',
onClick: (project) => {
window.open(projectEditorLink(project.proj_id), '_blank')
},
},
'separator',
{
text: 'Copy Link',
onClick: (project) => {
navigator.clipboard.writeText(projectEditorLink(project.proj_id))
// TODO notification toast
},
},
'separator',
{
text: 'Rename',
onClick: (project) => {
const newTitle = window.prompt('New title:', project.title)
if (newTitle != null) {
renameProject(project.proj_id, newTitle)
}
},
},
{
text: 'Delete',
onClick: (project) => {
deleteProject(project.proj_id)
},
},
]
case 'trash':
return [
{
text: 'Restore',
onClick: (project) => {
restoreProject(project.proj_id)
},
},
'separator',
{
text: 'Delete permanently',
onClick: (project) => {
destroyProject(project.proj_id)
},
},
]
default:
assertNever(selectedCategory)
}
}, [selectedCategory])

return (
<>
<DropdownMenu.Portal>
<DropdownMenu.Content
style={{
background: 'white',
padding: 4,
boxShadow: '2px 3px 4px #dddddd',
border: '1px solid #ccc',
borderRadius: 4,
display: 'flex',
flexDirection: 'column',
gap: 4,
minWidth: 100,
}}
sideOffset={5}
>
{menuEntries.map((entry, index) => {
if (entry === 'separator') {
return (
<DropdownMenu.Separator
key={`separator-${index}`}
style={{ backgroundColor: colors.separator, height: 1 }}
/>
)
}
return (
<DropdownMenu.Item
key={`entry-${index}`}
onClick={() => entry.onClick(project)}
className={contextMenuItem()}
>
{entry.text}
</DropdownMenu.Item>
)
})}
</DropdownMenu.Content>
</DropdownMenu.Portal>

<fetcher.Form />
</>
)
},
)
ProjectContextMenu.displayName = 'ProjectContextMenu'
4 changes: 4 additions & 0 deletions utopia-remix/app/radix-fix.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
body {
border: 1px solid transparent; /* Fix jumpy content when opening dropdowns */
}

2 changes: 2 additions & 0 deletions utopia-remix/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
import { BrowserEnvironment } from './env.server'
import { styles } from './styles/styles.css'

import './radix-fix.css'

declare global {
interface Window {
ENV: BrowserEnvironment
Expand Down
91 changes: 44 additions & 47 deletions utopia-remix/app/routes/projects.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { LoaderFunctionArgs, json } from '@remix-run/node'
import { useFetcher, useLoaderData } from '@remix-run/react'
import { useLoaderData } from '@remix-run/react'
import moment from 'moment'
import { UserDetails } from 'prisma-client'
import React, { useEffect, useState } from 'react'
import { ProjectContextMenu } from '../components/projectActionContextMenu'
import { listDeletedProjects, listProjects } from '../models/project.server'
import { newProjectButton } from '../styles/newProjectButton.css'
import { projectCategoryButton, userName } from '../styles/sidebarComponents.css'
import { sprinkles } from '../styles/sprinkles.css'
import { requireUser } from '../util/api.server'
import { ProjectWithoutContent } from '../types'
import { requireUser } from '../util/api.server'
import { assertNever } from '../util/assertNever'
import { projectEditorLink } from '../util/links'

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { button } from '../styles/button.css'

export async function loader(args: LoaderFunctionArgs) {
Expand Down Expand Up @@ -38,6 +42,7 @@ const categories: { [key in Category]: { name: string } } = {
allProjects: { name: 'All My Projects' },
trash: { name: 'Trash' },
}

const ProjectsPage = React.memo(() => {
const marginSize = 30
const rowHeight = 30
Expand Down Expand Up @@ -104,7 +109,7 @@ const ProjectsPage = React.memo(() => {
}, [searchValue, projects])

const createNewProject = () => {
window.open(`${window.ENV.EDITOR_URL}/project/`, '_blank')
window.open(projectEditorLink(null), '_blank')
}

const newProjectButtons = [
Expand Down Expand Up @@ -154,7 +159,7 @@ const ProjectsPage = React.memo(() => {
}
}, [])

const logoPic = isDarkMode ? 'url(assets/pyramid_dark.png)' : 'url(assets/pyramid_light.png)'
const logoPic = isDarkMode ? 'url(/assets/pyramid_dark.png)' : 'url(/assets/pyramid_light.png)'

return (
<div
Expand Down Expand Up @@ -335,6 +340,7 @@ const ProjectsPage = React.memo(() => {
project={project}
selected={project.proj_id === selectedProject.selectedProjectId}
onSelect={() => handleProjectSelect(project.proj_id)}
selectedCategory={selectedCategory}
/>
))}
</div>
Expand All @@ -350,11 +356,17 @@ type ProjectCardProps = {
project: ProjectWithoutContent
selected: boolean
onSelect: () => void
selectedCategory: Category
}

const ProjectCard: React.FC<ProjectCardProps> = ({ project, selected, onSelect }) => {
const ProjectCard: React.FC<ProjectCardProps> = ({
project,
selected,
onSelect,
selectedCategory,
}) => {
const openProject = React.useCallback(() => {
window.open(`${window.ENV.EDITOR_URL}/p/${project.proj_id}`, '_blank')
window.open(projectEditorLink(project.proj_id), '_blank')
}, [project.proj_id])

return (
Expand All @@ -381,50 +393,35 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, selected, onSelect }
onMouseDown={onSelect}
onDoubleClick={openProject}
/>
<ProjectActions project={project} />
<ProjectActions project={project} selectedCategory={selectedCategory} />
</div>
)
}

const ProjectActions = React.memo(({ project }: { project: ProjectWithoutContent }) => {
const fetcher = useFetcher()

const deleteProject = React.useCallback(() => {
if (project.deleted === true) {
const ok = window.confirm('Are you sure? The project contents will be deleted permanently.')
if (ok) {
fetcher.submit(
{},
{ method: 'POST', action: `/internal/projects/${project.proj_id}/destroy` },
)
}
} else {
fetcher.submit({}, { method: 'POST', action: `/internal/projects/${project.proj_id}/delete` })
}
}, [fetcher])

const restoreProject = React.useCallback(() => {
fetcher.submit({}, { method: 'POST', action: `/internal/projects/${project.proj_id}/restore` })
}, [fetcher])

return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', padding: 10, gap: 5, flex: 1 }}>
<div style={{ fontWeight: 600 }}>{project.title}</div>
<div>{moment(project.modified_at).fromNow()}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
{project.deleted === true ? (
<button className={button({ size: 'small' })} onClick={restoreProject}>
Restore
</button>
) : null}
<button className={button({ color: 'danger', size: 'small' })} onClick={deleteProject}>
Delete
</button>
<fetcher.Form />
const ProjectActions = React.memo(
({
project,
selectedCategory,
}: {
project: ProjectWithoutContent
selectedCategory: Category
}) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', padding: 10, gap: 5, flex: 1 }}>
<div style={{ fontWeight: 600 }}>{project.title}</div>
<div>{moment(project.modified_at).fromNow()}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button className={button()}></button>
</DropdownMenu.Trigger>
<ProjectContextMenu selectedCategory={selectedCategory} project={project} />
</DropdownMenu.Root>
</div>
</div>
</div>
)
})
)
},
)
ProjectActions.displayName = 'ProjectActions'
21 changes: 21 additions & 0 deletions utopia-remix/app/styles/contextMenuItem.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { recipe } from '@vanilla-extract/recipes'
import { colors, sprinkles } from './sprinkles.css'

export const contextMenuItem = recipe({
base: [
sprinkles({
borderRadius: 'small',
color: 'lightModeBlack',
}),
{
outline: 'none',
padding: '6px 8px',
cursor: 'pointer',
border: 'none !important',
':hover': {
backgroundColor: colors.primary,
color: 'white',
},
},
],
})
3 changes: 2 additions & 1 deletion utopia-remix/app/styles/sprinkles.css.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles'

const colors = {
export const colors = {
black: '#000',
white: '#fff',
primary: '#0075F9',
aqua: '#00E3E3',
darkModeBlack: '#181C20',
lightModeBlack: '#2B2B2B',
separator: '#dddddd',
}

const colorProperties = defineProperties({
Expand Down
9 changes: 9 additions & 0 deletions utopia-remix/app/util/links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import urlJoin from 'url-join'

export function projectEditorLink(projectId: string | null): string {
const editorURL = window.ENV.EDITOR_URL
if (editorURL == null) {
throw new Error('missing editor url')
}
return urlJoin(editorURL, 'project', projectId ?? '')
}
1 change: 1 addition & 0 deletions utopia-remix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"dependencies": {
"@prisma/client": "5.9.0",
"@radix-ui/react-dropdown-menu": "2.0.6",
"@remix-run/css-bundle": "2.5.1",
"@remix-run/node": "2.5.1",
"@remix-run/react": "2.5.1",
Expand Down
Loading

0 comments on commit df30077

Please sign in to comment.