Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ VITE_SERVER_API_KEY=""
# (e.g. https://mpr.lt/c/<challengeId>/t/<taskId>). If unset,
# short links will be omitted from changeset comments.
VITE_SHORT_URL="https://mpr.lt"
# Comma-separated URLs of plugin bundles to auto-load at login (e.g. maproulette-review)
# VITE_DEPLOYMENT_PLUGIN_URLS="http://localhost:4201/maprouletteReviewPlugin.js"
7 changes: 2 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@
<body>
<div id="app" tabindex="-1"></div>
<script type="module">
// Load runtime config into window.env, then start the app
// Load runtime config, expose host React for plugins, then boot the app
window.env = await fetch('/env.json').then((res) => res.json());
await import('/src/main.tsx');
</script>
<script type="module">
// Expose React globallt for plugins after the main app loads
import React from 'react';
import ReactDOM from 'react-dom';
import * as jsxRuntime from 'react/jsx-runtime';
window.React = React;
window.ReactDOM = ReactDOM;
window.jsxRuntime = jsxRuntime;
await import('/src/main.tsx');
</script>
</body>

Expand Down
39 changes: 38 additions & 1 deletion src/components/Pages/TaskEditPage/TaskActionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import {
SelectValue,
} from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea'
import { usePluginContext } from '@/contexts/PluginContext'
import { logger } from '@/lib/logger'
import { STATUS_LABELS } from '@/lib/taskConstants'
import type { TaskActionExtension } from '@/types/Plugin'
import type { Task } from '@/types/Task'
import { PENDING_BUNDLE_ID, useTaskBundleContext } from './contexts/TaskBundleContext'
import { TaskNearbyMap } from './TaskNearbyMap'
Expand Down Expand Up @@ -53,6 +55,7 @@ export const TaskActionModal = ({
}: TaskActionModalProps) => {
const queryClient = useQueryClient()
const navigate = useNavigate()
const { getTaskActionExtensions } = usePluginContext()
const commentId = useId()
const tagsId = useId()
const randomId = useId()
Expand All @@ -62,6 +65,8 @@ export const TaskActionModal = ({
const [tags, setTags] = useState('')
const [nextTaskType, setNextTaskType] = useState<'nearby' | 'random'>('random')
const [selectedNearbyTaskId, setSelectedNearbyTaskId] = useState<number | null>(null)
const [formState, setFormState] = useState<Record<string, unknown>>({})
const [extensions, setExtensions] = useState<TaskActionExtension[]>([])
const [isSubmitting, setIsSubmitting] = useState(false)
const addTaskCommentMutation = api.task.useAddTaskComment()
const updateTaskStatusMutation = api.task.useUpdateTaskStatus()
Expand All @@ -71,11 +76,24 @@ export const TaskActionModal = ({
const { activeBundle, initialBundle } = useTaskBundleContext()
const currentStatus = task.status ?? 0
const currentStatusLabel = STATUS_LABELS[currentStatus] || 'Unknown'

useEffect(() => {
setNewStatus(initialStatus)
}, [initialStatus])

useEffect(() => {
let cancelled = false
const loadExtensions = async () => {
const results = await getTaskActionExtensions()
if (!cancelled) {
setExtensions(results)
}
}
void loadExtensions()
return () => {
cancelled = true
}
}, [getTaskActionExtensions])

const handleSubmit = async () => {
try {
setIsSubmitting(true)
Expand Down Expand Up @@ -175,6 +193,7 @@ export const TaskActionModal = ({
setNewStatus(initialStatus)
setNextTaskType('random')
setSelectedNearbyTaskId(null)
setFormState({})
onOpenChange(false)
}

Expand Down Expand Up @@ -239,6 +258,24 @@ export const TaskActionModal = ({
<p className="text-xs text-zinc-500">Separate multiple tags with commas</p>
</div>

{extensions.map((extension) => {
const ExtensionComponent = extension.component
return (
<div
key={extension.id}
className="space-y-2 rounded-lg border border-zinc-200 p-3 dark:border-slate-700"
>
<ExtensionComponent
task={task}
newStatus={newStatus}
setNewStatus={setNewStatus}
formState={formState}
setFormState={(patch) => setFormState((prev) => ({ ...prev, ...patch }))}
/>
</div>
)
})}

{/* Next Task Selection */}
<div className="space-y-3">
<Label>Next Task</Label>
Expand Down
54 changes: 53 additions & 1 deletion src/components/Pages/TaskEditPage/TaskPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useLocation } from '@tanstack/react-router'
import { useEffect, useRef, useState } from 'react'
import { api } from '@/api'
import { isTaskEligibleForBundle } from '@/components/Map/TaskMarkers/utils'
Expand All @@ -12,19 +13,24 @@ import { TaskTab } from '@/components/TaskInfoPanel/TaskTab/TaskTab'
import { TaskTabs } from '@/components/TaskInfoPanel/TaskTabs'
import { Drawer } from '@/components/ui/Drawer'
import { useAuthContext } from '@/contexts/AuthContext'
import { usePluginContext } from '@/contexts/PluginContext'
import type { TaskActionPanelExtension } from '@/types/Plugin'
import type { Task, TaskMarker } from '@/types/Task'
import { TaskActions } from './TaskActions/TaskActions'
import { TaskInfoHeader } from './TaskInfoHeader'

export const TaskPanel = () => {
const location = useLocation()
const { task, isLocked } = useTaskContext()
const { getTaskActionPanels } = usePluginContext()
const { user } = useAuthContext()
const { activeBundle, setActiveBundle, setInitialBundle, bundleEditsDisabled } =
useTaskBundleContext()
const { selectedMarker, setSelectedMarker, setActiveTaskId, emptyClickCount } =
useTaskMapContext()
const { highlightIdEntityRef, activeView } = useEditorContext()
const [drawerTaskId, setDrawerTaskId] = useState<number | null>(null)
const [taskActionPanels, setTaskActionPanels] = useState<TaskActionPanelExtension[]>([])
// 'closed' | 'open' | 'sliding-out' (animating out before switching task)
const [drawerState, setDrawerState] = useState<'closed' | 'open' | 'sliding-out'>('closed')

Expand All @@ -39,6 +45,20 @@ export const TaskPanel = () => {
const prevTargetRef = useRef(targetTaskId)
const drawerStateRef = useRef(drawerState)
drawerStateRef.current = drawerState
useEffect(() => {
let cancelled = false
const loadTaskActionPanels = async () => {
const results = await getTaskActionPanels()
if (!cancelled) {
setTaskActionPanels(results)
}
}
void loadTaskActionPanels()
return () => {
cancelled = true
}
}, [getTaskActionPanels])

useEffect(() => {
const prevTarget = prevTargetRef.current
prevTargetRef.current = targetTaskId
Expand Down Expand Up @@ -173,6 +193,11 @@ export const TaskPanel = () => {
}

const isViewedTaskInBundle = activeBundle?.taskIds.includes(viewedTaskId) ?? false
const search = (location.search as Record<string, unknown>) ?? {}
const panelContext = { pathname: location.pathname, search, task }
const activePanels = taskActionPanels.filter((panel) => panel.isActive?.(panelContext) ?? true)
const replacePanels = activePanels.filter((panel) => panel.slot === 'replace')
const appendPanels = activePanels.filter((panel) => panel.slot !== 'replace')

// Check if the selected marker is eligible for bundling
const isSelectedMarkerEligible =
Expand Down Expand Up @@ -207,7 +232,34 @@ export const TaskPanel = () => {

{/* Task Actions Footer - floats over content, under drawer */}
<div className="absolute right-0 bottom-0 left-0 z-10 rounded-b-2xl border-slate-200/80 border-t bg-white px-3 pt-3 pb-3 dark:border-slate-700/50 dark:bg-slate-800">
<TaskActions />
{replacePanels.length > 0 ? (
replacePanels.map((panel) => {
const PanelComponent = panel.component
return (
<PanelComponent
key={panel.id}
task={task}
search={search}
pathname={location.pathname}
/>
)
})
) : (
<>
<TaskActions />
{appendPanels.map((panel) => {
const PanelComponent = panel.component
return (
<PanelComponent
key={panel.id}
task={task}
search={search}
pathname={location.pathname}
/>
)
})}
</>
)}
</div>

{/* Drawer overlay for non-primary tasks */}
Expand Down
4 changes: 3 additions & 1 deletion src/components/Pages/TaskEditPage/contexts/TaskContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export interface TaskContextType {
export const TaskContext = createContext<TaskContextType | undefined>(undefined)

export const TaskProvider = ({ children }: { children: ReactNode }) => {
const { task } = useLoaderData({ from: '/_app/tasks/$taskId/' })
const { task: loaderTask } = useLoaderData({ from: '/_app/tasks/$taskId/' })
const { data: cachedTask } = api.task.getTask(loaderTask.id)
const task = cachedTask ?? loaderTask
const { isAuthenticated } = useAuthContext()
const lockTaskMutation = api.task.useLockTask()
const unlockTaskMutation = api.task.useUnlockTask()
Expand Down
Loading
Loading