diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7757bfbc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.go] +indent_size = 4 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..432b368c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto eol=lf +*.go text eol=lf +*.js text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.sh text eol=lf +*.bat text eol=crlf \ No newline at end of file diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 00000000..7de69380 --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100 +} diff --git a/ui/README.md b/ui/README.md index 8ba6cb99..b65c1206 100644 --- a/ui/README.md +++ b/ui/README.md @@ -17,9 +17,9 @@ If you are developing a production application, we recommend updating the config ```js export default defineConfig([ - globalIgnores(["dist"]), + globalIgnores(['dist']), { - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], extends: [ // Other configs... @@ -34,7 +34,7 @@ export default defineConfig([ ], languageOptions: { parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], + project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, // other options... @@ -47,23 +47,23 @@ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-re ```js // eslint.config.js -import reactX from "eslint-plugin-react-x"; -import reactDom from "eslint-plugin-react-dom"; +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; export default defineConfig([ - globalIgnores(["dist"]), + globalIgnores(['dist']), { - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], extends: [ // Other configs... // Enable lint rules for React - reactX.configs["recommended-typescript"], + reactX.configs['recommended-typescript'], // Enable lint rules for React DOM reactDom.configs.recommended, ], languageOptions: { parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], + project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, // other options... diff --git a/ui/eslint.config.js b/ui/eslint.config.js index 75d3c46f..a53e3817 100644 --- a/ui/eslint.config.js +++ b/ui/eslint.config.js @@ -1,14 +1,14 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; -import { defineConfig, globalIgnores } from "eslint/config"; +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ - globalIgnores(["dist"]), + globalIgnores(['dist']), { - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], extends: [ js.configs.recommended, tseslint.configs.recommended, diff --git a/ui/package-lock.json b/ui/package-lock.json index f56c742e..6ee77b87 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "Devlane UI", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Devlane UI", - "version": "0.4.0", + "version": "0.4.1", "dependencies": { "@headlessui/react": "^2.2.9", "@tailwindcss/vite": "^4.1.18", diff --git a/ui/package.json b/ui/package.json index 32c8e940..c642d5dd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "Devlane UI", "private": true, - "version": "0.4.0", + "version": "0.4.1", "type": "module", "scripts": { "dev": "vite", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 089c692c..55c9363b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,8 +1,8 @@ -import { RouterProvider } from "react-router-dom"; -import { AuthProvider } from "./contexts/AuthContext"; -import { FavoritesProvider } from "./contexts/FavoritesContext"; -import { ThemeProvider } from "./contexts/ThemeContext"; -import { router } from "./routes"; +import { RouterProvider } from 'react-router-dom'; +import { AuthProvider } from './contexts/AuthContext'; +import { FavoritesProvider } from './contexts/FavoritesContext'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { router } from './routes'; export default function App() { return ( diff --git a/ui/src/api/client.ts b/ui/src/api/client.ts index 531f45a3..a29506d4 100644 --- a/ui/src/api/client.ts +++ b/ui/src/api/client.ts @@ -1,5 +1,5 @@ -import axios, { type AxiosError } from "axios"; -import { config } from "../config/env"; +import axios, { type AxiosError } from 'axios'; +import { config } from '../config/env'; /** * Shared Axios instance for all API requests. @@ -11,7 +11,7 @@ export const apiClient = axios.create({ baseURL: config.apiBaseUrl, withCredentials: true, headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, }); @@ -21,7 +21,7 @@ export const apiClient = axios.create({ apiClient.interceptors.request.use((config) => { if (config.data instanceof FormData && config.headers) { const h = config.headers as Record; - delete h["Content-Type"]; + delete h['Content-Type']; } return config; }); @@ -46,7 +46,7 @@ export function getApiErrorMessage(err: unknown): string { if (ax.response?.status) return `Request failed (${ax.response.status}).`; } if (err instanceof Error) return err.message; - return "An unexpected error occurred."; + return 'An unexpected error occurred.'; } apiClient.interceptors.response.use( diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index c7ef7d2a..85a8a41b 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,3 +1,3 @@ -export { apiClient, getApiErrorMessage } from "./client"; -export type { ApiErrorResponse } from "./client"; -export type { CreateWorkspaceRequest, WorkspaceApiResponse } from "./types"; +export { apiClient, getApiErrorMessage } from './client'; +export type { ApiErrorResponse } from './client'; +export type { CreateWorkspaceRequest, WorkspaceApiResponse } from './types'; diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 4d73a0ec..056f27fb 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -391,7 +391,7 @@ export interface IssueViewApiResponse { filters?: Record; display_filters?: Record; display_properties?: Record; - access?: number | "public" | "private"; + access?: number | 'public' | 'private'; sort_order?: number; anchor?: string | null; is_favorite?: boolean; diff --git a/ui/src/components/AddExistingWorkItemModal.tsx b/ui/src/components/AddExistingWorkItemModal.tsx index 47627038..3339f5d8 100644 --- a/ui/src/components/AddExistingWorkItemModal.tsx +++ b/ui/src/components/AddExistingWorkItemModal.tsx @@ -1,9 +1,9 @@ -import { useEffect, useState, useMemo } from "react"; -import { createPortal } from "react-dom"; -import { Button } from "./ui"; -import { issueService } from "../services/issueService"; -import { moduleService } from "../services/moduleService"; -import type { IssueApiResponse } from "../api/types"; +import { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { Button } from './ui'; +import { issueService } from '../services/issueService'; +import { moduleService } from '../services/moduleService'; +import type { IssueApiResponse } from '../api/types'; const IconSearch = () => ( >(new Set()); const [projectIssues, setProjectIssues] = useState([]); const [moduleIssueIds, setModuleIssueIds] = useState>(new Set()); @@ -67,7 +67,7 @@ export function AddExistingWorkItemModal({ setSelectedIds(new Set()); }) .catch(() => { - if (!cancelled) setError("Failed to load work items"); + if (!cancelled) setError('Failed to load work items'); }) .finally(() => { if (!cancelled) setLoading(false); @@ -79,7 +79,7 @@ export function AddExistingWorkItemModal({ useEffect(() => { if (!open) { - setSearchQuery(""); + setSearchQuery(''); setSelectedIds(new Set()); } }, [open]); @@ -93,7 +93,7 @@ export function AddExistingWorkItemModal({ const q = searchQuery.trim().toLowerCase(); return availableIssues.filter((issue) => { const id = displayId(issue, projectIdentifier); - const name = (issue.name ?? "").toLowerCase(); + const name = (issue.name ?? '').toLowerCase(); return id.toLowerCase().includes(q) || name.includes(q); }); }, [availableIssues, searchQuery, projectIdentifier]); @@ -127,20 +127,13 @@ export function AddExistingWorkItemModal({ await Promise.all( ids .slice(i, i + BATCH) - .map((issueId) => - moduleService.addIssue( - workspaceSlug, - projectId, - moduleId, - issueId, - ), - ), + .map((issueId) => moduleService.addIssue(workspaceSlug, projectId, moduleId, issueId)), ); } onAdded?.(); onClose(); } catch { - setError("Failed to add work items"); + setError('Failed to add work items'); } finally { setSubmitting(false); } @@ -149,8 +142,8 @@ export function AddExistingWorkItemModal({ const selectedCount = selectedIds.size; const selectionLabel = selectedCount === 0 - ? "No work items selected" - : `${selectedCount} work item${selectedCount === 1 ? "" : "s"} selected`; + ? 'No work items selected' + : `${selectedCount} work item${selectedCount === 1 ? '' : 's'} selected`; const footer = (
@@ -185,11 +178,7 @@ export function AddExistingWorkItemModal({ aria-modal="true" aria-labelledby="add-existing-work-item-title" > -
+
e.stopPropagation()} @@ -217,18 +206,14 @@ export function AddExistingWorkItemModal({
- {error && ( -

{error}

- )} + {error &&

{error}

} {loading ? ( -

- Loading work items… -

+

Loading work items…

) : filteredIssues.length === 0 ? (

{availableIssues.length === 0 - ? "No other work items in this project." - : "No matching work items."} + ? 'No other work items in this project.' + : 'No matching work items.'}

) : (
    @@ -249,12 +234,8 @@ export function AddExistingWorkItemModal({ aria-hidden /> - - {id} - - - {issue.name || "—"} - + {id} + {issue.name || '—'} @@ -264,9 +245,7 @@ export function AddExistingWorkItemModal({ )}
-
- {footer} -
+
{footer}
, document.body, diff --git a/ui/src/components/CoverImageModal.tsx b/ui/src/components/CoverImageModal.tsx index 3fdae6a3..43ac0c99 100644 --- a/ui/src/components/CoverImageModal.tsx +++ b/ui/src/components/CoverImageModal.tsx @@ -1,13 +1,10 @@ -import { useCallback, useEffect, useState } from "react"; -import { Button, Modal } from "./ui"; -import { - instanceSettingsService, - type UnsplashSearchResult, -} from "../services/instanceService"; -import { uploadImage } from "../services/uploadService"; +import { useCallback, useEffect, useState } from 'react'; +import { Button, Modal } from './ui'; +import { instanceSettingsService, type UnsplashSearchResult } from '../services/instanceService'; +import { uploadImage } from '../services/uploadService'; -const TAB_UNSPLASH = "unsplash"; -const TAB_UPLOAD = "upload"; +const TAB_UNSPLASH = 'unsplash'; +const TAB_UPLOAD = 'upload'; type Tab = typeof TAB_UNSPLASH | typeof TAB_UPLOAD; export interface CoverImageModalProps { @@ -21,13 +18,11 @@ export function CoverImageModal({ open, onClose, onSelect, - title = "Select cover image", + title = 'Select cover image', }: CoverImageModalProps) { const [tab, setTab] = useState(TAB_UNSPLASH); - const [unsplashQuery, setUnsplashQuery] = useState(""); - const [unsplashResults, setUnsplashResults] = useState< - UnsplashSearchResult[] - >([]); + const [unsplashQuery, setUnsplashQuery] = useState(''); + const [unsplashResults, setUnsplashResults] = useState([]); const [unsplashLoading, setUnsplashLoading] = useState(false); const [unsplashError, setUnsplashError] = useState(null); const [selectedUrl, setSelectedUrl] = useState(null); @@ -39,7 +34,7 @@ export function CoverImageModal({ useEffect(() => { if (!open) { setTab(TAB_UNSPLASH); - setUnsplashQuery(""); + setUnsplashQuery(''); setUnsplashResults([]); setUnsplashError(null); setSelectedUrl(null); @@ -54,12 +49,10 @@ export function CoverImageModal({ setUnsplashError(null); setUnsplashLoading(true); try { - const { results } = await instanceSettingsService.unsplashSearch( - unsplashQuery.trim(), - ); + const { results } = await instanceSettingsService.unsplashSearch(unsplashQuery.trim()); setUnsplashResults(results); } catch (e) { - setUnsplashError(e instanceof Error ? e.message : "Search failed"); + setUnsplashError(e instanceof Error ? e.message : 'Search failed'); setUnsplashResults([]); } finally { setUnsplashLoading(false); @@ -73,33 +66,28 @@ export function CoverImageModal({ } }, [selectedUrl, onSelect, onClose]); - const handleFileChange = useCallback( - (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - const allowed = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; - if (!allowed.includes(file.type)) { - setUploadError( - "Invalid file type. Supported: .jpeg, .jpg, .png, .webp", - ); - return; - } - setUploadError(null); - setUploadFile(file); - const reader = new FileReader(); - reader.onload = () => setUploadPreview(reader.result as string); - reader.readAsDataURL(file); - }, - [], - ); + const handleFileChange = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const allowed = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + if (!allowed.includes(file.type)) { + setUploadError('Invalid file type. Supported: .jpeg, .jpg, .png, .webp'); + return; + } + setUploadError(null); + setUploadFile(file); + const reader = new FileReader(); + reader.onload = () => setUploadPreview(reader.result as string); + reader.readAsDataURL(file); + }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); const file = e.dataTransfer.files?.[0]; if (!file) return; - const allowed = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; + const allowed = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; if (!allowed.includes(file.type)) { - setUploadError("Invalid file type. Supported: .jpeg, .jpg, .png, .webp"); + setUploadError('Invalid file type. Supported: .jpeg, .jpg, .png, .webp'); return; } setUploadError(null); @@ -109,10 +97,7 @@ export function CoverImageModal({ reader.readAsDataURL(file); }, []); - const handleDragOver = useCallback( - (e: React.DragEvent) => e.preventDefault(), - [], - ); + const handleDragOver = useCallback((e: React.DragEvent) => e.preventDefault(), []); const handleUploadSave = useCallback(async () => { if (!uploadFile) return; @@ -123,7 +108,7 @@ export function CoverImageModal({ onSelect(url); onClose(); } catch (e) { - setUploadError(e instanceof Error ? e.message : "Upload failed"); + setUploadError(e instanceof Error ? e.message : 'Upload failed'); } finally { setUploadLoading(false); } @@ -146,32 +131,23 @@ export function CoverImageModal({ )} {tab === TAB_UPLOAD && ( - )} ); return ( - +
- {unsplashError && ( -

- {unsplashError} -

- )} + {unsplashError &&

{unsplashError}

}
{unsplashResults.map((r) => ( ))}
@@ -244,9 +208,7 @@ export function CoverImageModal({ onDragOver={handleDragOver} className="flex flex-col items-center justify-center rounded-(--radius-md) border-2 border-dashed border-(--border-subtle) bg-(--bg-layer-2) py-12 px-4" > -

- Drag & drop image here -

+

Drag & drop image here

) : (
- Preview + Preview
)} diff --git a/ui/src/components/CreateModuleModal.tsx b/ui/src/components/CreateModuleModal.tsx index 68b6d8d6..6abb77e7 100644 --- a/ui/src/components/CreateModuleModal.tsx +++ b/ui/src/components/CreateModuleModal.tsx @@ -1,26 +1,22 @@ -import { useState, useEffect, useRef } from "react"; -import { Modal, Button, Input, Avatar } from "./ui"; -import { DateRangeModal } from "./workspace-views/DateRangeModal"; -import { getImageUrl } from "../lib/utils"; -import { moduleService } from "../services/moduleService"; -import { workspaceService } from "../services/workspaceService"; -import type { ModuleApiResponse } from "../api/types"; -import type { WorkspaceMemberApiResponse } from "../api/types"; -import { formatISODateDisplay } from "../lib/dateOnly"; -import { MODULE_STATUSES } from "../lib/moduleStatuses"; +import { useState, useEffect, useRef } from 'react'; +import { Modal, Button, Input, Avatar } from './ui'; +import { DateRangeModal } from './workspace-views/DateRangeModal'; +import { getImageUrl } from '../lib/utils'; +import { moduleService } from '../services/moduleService'; +import { workspaceService } from '../services/workspaceService'; +import type { ModuleApiResponse } from '../api/types'; +import type { WorkspaceMemberApiResponse } from '../api/types'; +import { formatISODateDisplay } from '../lib/dateOnly'; +import { MODULE_STATUSES } from '../lib/moduleStatuses'; -function formatDateRangeDisplay( - start: string | null, - end: string | null, -): string { - if (!start && !end) return "Start date → End date"; - if (start && end) - return `${formatISODateDisplay(start)} → ${formatISODateDisplay(end)}`; +function formatDateRangeDisplay(start: string | null, end: string | null): string { + if (!start && !end) return 'Start date → End date'; + if (start && end) return `${formatISODateDisplay(start)} → ${formatISODateDisplay(end)}`; return start ? formatISODateDisplay(start) : end ? formatISODateDisplay(end) - : "Start date → End date"; + : 'Start date → End date'; } export interface CreateModuleModalProps { @@ -40,19 +36,19 @@ export function CreateModuleModal({ projectName, onCreated, }: CreateModuleModalProps) { - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); - const [status, setStatus] = useState("backlog"); + const [status, setStatus] = useState('backlog'); const [leadId, setLeadId] = useState(null); const [memberIds, setMemberIds] = useState([]); const [dateModalOpen, setDateModalOpen] = useState(false); const [statusDropdownOpen, setStatusDropdownOpen] = useState(false); const [leadDropdownOpen, setLeadDropdownOpen] = useState(false); const [membersDropdownOpen, setMembersDropdownOpen] = useState(false); - const [leadSearch, setLeadSearch] = useState(""); - const [membersSearch, setMembersSearch] = useState(""); + const [leadSearch, setLeadSearch] = useState(''); + const [membersSearch, setMembersSearch] = useState(''); const [members, setMembers] = useState([]); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -70,16 +66,16 @@ export function CreateModuleModal({ useEffect(() => { if (!open) { - setTitle(""); - setDescription(""); + setTitle(''); + setDescription(''); setStartDate(null); setEndDate(null); - setStatus("backlog"); + setStatus('backlog'); setLeadId(null); setMemberIds([]); setError(null); - setLeadSearch(""); - setMembersSearch(""); + setLeadSearch(''); + setMembersSearch(''); setStatusDropdownOpen(false); setLeadDropdownOpen(false); setMembersDropdownOpen(false); @@ -96,26 +92,22 @@ export function CreateModuleModal({ setLeadDropdownOpen(false); setMembersDropdownOpen(false); }; - document.addEventListener("mousedown", handler); - return () => document.removeEventListener("mousedown", handler); + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); }, []); const q = (s: string) => s.trim().toLowerCase(); const filteredLead = members.filter((m) => - q(m.member_display_name ?? m.member_email ?? m.member_id).includes( - q(leadSearch), - ), + q(m.member_display_name ?? m.member_email ?? m.member_id).includes(q(leadSearch)), ); const filteredMembers = members.filter((m) => - q(m.member_display_name ?? m.member_email ?? m.member_id).includes( - q(membersSearch), - ), + q(m.member_display_name ?? m.member_email ?? m.member_id).includes(q(membersSearch)), ); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!title.trim()) { - setError("Title is required."); + setError('Title is required.'); return; } setError(null); @@ -124,24 +116,21 @@ export function CreateModuleModal({ const created = await moduleService.create(workspaceSlug, projectId, { name: title.trim(), description: description.trim() || undefined, - status: status || "backlog", + status: status || 'backlog', start_date: startDate || undefined, target_date: endDate || undefined, }); onClose(); onCreated?.(created); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create module."); + setError(err instanceof Error ? err.message : 'Failed to create module.'); } finally { setSubmitting(false); } }; - const statusLabel = - MODULE_STATUSES.find((s) => s.id === status)?.label ?? status; - const leadMember = leadId - ? members.find((m) => m.member_id === leadId) - : null; + const statusLabel = MODULE_STATUSES.find((s) => s.id === status)?.label ?? status; + const leadMember = leadId ? members.find((m) => m.member_id === leadId) : null; const selectedMembers = memberIds .map((id) => members.find((m) => m.member_id === id)) .filter(Boolean) as WorkspaceMemberApiResponse[]; @@ -158,22 +147,14 @@ export function CreateModuleModal({ - } >
{projectName}
-
+ { - setLeadId( - leadId === m.member_id ? null : m.member_id, - ); + setLeadId(leadId === m.member_id ? null : m.member_id); setLeadDropdownOpen(false); }} className="flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-sm text-(--txt-primary) hover:bg-(--bg-layer-1-hover)" > (
- {error && ( -

{error}

- )} + {error &&

{error}

} diff --git a/ui/src/components/CreateProjectModal.tsx b/ui/src/components/CreateProjectModal.tsx index 1a59bece..4d56cad4 100644 --- a/ui/src/components/CreateProjectModal.tsx +++ b/ui/src/components/CreateProjectModal.tsx @@ -1,21 +1,18 @@ -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; -import { Button, Input } from "./ui"; -import { CoverImageModal } from "./CoverImageModal"; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Button, Input } from './ui'; +import { CoverImageModal } from './CoverImageModal'; import { ProjectIconDisplay, ProjectIconModal, type ProjectIconSelection, -} from "./ProjectIconModal"; -import { projectService } from "../services/projectService"; -import type { - ProjectApiResponse, - WorkspaceMemberApiResponse, -} from "../api/types"; -import { useAuth } from "../contexts/AuthContext"; -import { workspaceService } from "../services/workspaceService"; -import { ProjectNetworkSelect } from "./ProjectNetworkSelect"; -import { ProjectLeadSelect } from "./ProjectLeadSelect"; +} from './ProjectIconModal'; +import { projectService } from '../services/projectService'; +import type { ProjectApiResponse, WorkspaceMemberApiResponse } from '../api/types'; +import { useAuth } from '../contexts/AuthContext'; +import { workspaceService } from '../services/workspaceService'; +import { ProjectNetworkSelect } from './ProjectNetworkSelect'; +import { ProjectLeadSelect } from './ProjectLeadSelect'; export interface CreateProjectModalProps { open: boolean; @@ -25,11 +22,11 @@ export interface CreateProjectModalProps { } const COVER_GRADIENTS = [ - "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)", - "linear-gradient(135deg, #0ea5e9 0%, #38bdf8 50%, #7dd3fc 100%)", - "linear-gradient(135deg, #10b981 0%, #34d399 50%, #6ee7b7 100%)", - "linear-gradient(135deg, #f59e0b 0%, #fbbf24 50%, #fcd34d 100%)", - "linear-gradient(135deg, #ec4899 0%, #f472b6 50%, #f9a8d4 100%)", + 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)', + 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 50%, #7dd3fc 100%)', + 'linear-gradient(135deg, #10b981 0%, #34d399 50%, #6ee7b7 100%)', + 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 50%, #fcd34d 100%)', + 'linear-gradient(135deg, #ec4899 0%, #f472b6 50%, #f9a8d4 100%)', ]; const IconInfo = () => ( @@ -74,41 +71,38 @@ export function CreateProjectModal({ onSuccess, }: CreateProjectModalProps) { const { user } = useAuth(); - const [name, setName] = useState(""); - const [identifier, setIdentifier] = useState(""); - const [description, setDescription] = useState(""); + const [name, setName] = useState(''); + const [identifier, setIdentifier] = useState(''); + const [description, setDescription] = useState(''); const [emoji, setEmoji] = useState(null); - const [iconProp, setIconProp] = - useState(null); + const [iconProp, setIconProp] = useState(null); const [coverImage, setCoverImage] = useState(null); - const [network, setNetwork] = useState<"public" | "private">("public"); + const [network, setNetwork] = useState<'public' | 'private'>('public'); const [projectLeadId, setProjectLeadId] = useState(null); - const [workspaceMembers, setWorkspaceMembers] = useState< - WorkspaceMemberApiResponse[] - >([]); - const [error, setError] = useState(""); + const [workspaceMembers, setWorkspaceMembers] = useState([]); + const [error, setError] = useState(''); const [submitting, setSubmitting] = useState(false); const [coverModalOpen, setCoverModalOpen] = useState(false); const [iconModalOpen, setIconModalOpen] = useState(false); const handleClose = () => { - setName(""); - setIdentifier(""); - setDescription(""); + setName(''); + setIdentifier(''); + setDescription(''); setEmoji(null); setIconProp(null); setCoverImage(null); - setNetwork("public"); + setNetwork('public'); setProjectLeadId(null); - setError(""); + setError(''); onClose(); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setError(""); + setError(''); if (!name.trim()) { - setError("Project name is required."); + setError('Project name is required.'); return; } setSubmitting(true); @@ -120,7 +114,7 @@ export function CreateProjectModal({ cover_image: coverImage || undefined, emoji: emoji ?? undefined, icon_prop: iconProp ?? undefined, - guest_view_all_features: network === "public" ? true : undefined, + guest_view_all_features: network === 'public' ? true : undefined, project_lead_id: projectLeadId ?? undefined, }; @@ -128,9 +122,7 @@ export function CreateProjectModal({ onSuccess?.(project); handleClose(); } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to create project.", - ); + setError(err instanceof Error ? err.message : 'Failed to create project.'); } finally { setSubmitting(false); } @@ -140,10 +132,10 @@ export function CreateProjectModal({ if (!open) return; const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); + if (e.key === 'Escape') onClose(); }; - document.addEventListener("keydown", handleEscape); - document.body.style.overflow = "hidden"; + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; // Load workspace members for project lead dropdown workspaceService @@ -154,8 +146,8 @@ export function CreateProjectModal({ }); return () => { - document.removeEventListener("keydown", handleEscape); - document.body.style.overflow = ""; + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = ''; }; }, [open, onClose, workspaceSlug]); @@ -165,8 +157,8 @@ export function CreateProjectModal({ coverImage != null ? { backgroundImage: `url(${coverImage})`, - backgroundSize: "cover", - backgroundPosition: "center", + backgroundSize: 'cover', + backgroundPosition: 'center', } : { background: COVER_GRADIENTS[0], @@ -182,11 +174,7 @@ export function CreateProjectModal({

Create project

-
+
e.stopPropagation()} @@ -244,9 +232,7 @@ export function CreateProjectModal({ - setIdentifier( - e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, ""), - ) + setIdentifier(e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '')) } placeholder="e.g. PROJ" disabled={submitting} @@ -273,11 +259,7 @@ export function CreateProjectModal({ {/* Network + Project Lead (under description, side by side) */}
- +
- {error && ( -

{error}

- )} + {error &&

{error}

} {/* Actions */}
-
diff --git a/ui/src/components/CreateWorkItemModal.tsx b/ui/src/components/CreateWorkItemModal.tsx index 5ad0c458..4f2aa582 100644 --- a/ui/src/components/CreateWorkItemModal.tsx +++ b/ui/src/components/CreateWorkItemModal.tsx @@ -1,56 +1,35 @@ -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; -import { Button, Input } from "./ui"; -import { Dropdown, DatePickerTrigger, SelectParentModal } from "./work-item"; -import { stateService } from "../services/stateService"; -import { labelService } from "../services/labelService"; -import { issueService } from "../services/issueService"; -import { cycleService } from "../services/cycleService"; -import { moduleService } from "../services/moduleService"; -import { workspaceService } from "../services/workspaceService"; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Button, Input } from './ui'; +import { Dropdown, DatePickerTrigger, SelectParentModal } from './work-item'; +import { stateService } from '../services/stateService'; +import { labelService } from '../services/labelService'; +import { issueService } from '../services/issueService'; +import { cycleService } from '../services/cycleService'; +import { moduleService } from '../services/moduleService'; +import { workspaceService } from '../services/workspaceService'; import type { StateApiResponse, LabelApiResponse, IssueApiResponse, ProjectApiResponse, -} from "../api/types"; -import type { Priority } from "../types"; +} from '../api/types'; +import type { Priority } from '../types'; const IconCog = () => ( - + ); const IconCircleSlash = () => ( - + ); const IconUsers = () => ( - + @@ -58,26 +37,12 @@ const IconUsers = () => ( ); const IconTag = () => ( - + ); const IconCalendar = () => ( - + @@ -85,27 +50,13 @@ const IconCalendar = () => ( ); const IconCycle = () => ( - + ); const IconGrid = () => ( - + @@ -113,41 +64,20 @@ const IconGrid = () => ( ); const IconLink2 = () => ( - + ); const IconTruck = () => ( - + ); const IconBuilding = () => ( - + @@ -162,7 +92,7 @@ const IconBuilding = () => ( ); -const PRIORITIES: Priority[] = ["urgent", "high", "medium", "low", "none"]; +const PRIORITIES: Priority[] = ['urgent', 'high', 'medium', 'low', 'none']; export interface CreateWorkItemModalProps { open: boolean; @@ -199,63 +129,54 @@ export function CreateWorkItemModal({ createError, onSave, }: CreateWorkItemModalProps) { - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - const [projectId, setProjectId] = useState( - defaultProjectId ?? projects[0]?.id ?? "", - ); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [projectId, setProjectId] = useState(defaultProjectId ?? projects[0]?.id ?? ''); const [createMore, setCreateMore] = useState(false); const [submitting, setSubmitting] = useState(false); const [openDropdown, setOpenDropdown] = useState(null); - const [stateId, setStateId] = useState(""); - const [priority, setPriority] = useState("none"); + const [stateId, setStateId] = useState(''); + const [priority, setPriority] = useState('none'); const [assigneeIds, setAssigneeIds] = useState([]); const [labelIds, setLabelIds] = useState([]); - const [startDate, setStartDate] = useState(""); - const [dueDate, setDueDate] = useState(""); + const [startDate, setStartDate] = useState(''); + const [dueDate, setDueDate] = useState(''); const [cycleId, setCycleId] = useState(null); - const [moduleId, setModuleId] = useState( - defaultModuleId ?? null, - ); + const [moduleId, setModuleId] = useState(defaultModuleId ?? null); const [parentId, setParentId] = useState(null); const [parentModalOpen, setParentModalOpen] = useState(false); - const [projectSearch, setProjectSearch] = useState(""); - const [stateSearch, setStateSearch] = useState(""); - const [assigneeSearch, setAssigneeSearch] = useState(""); - const [labelSearch, setLabelSearch] = useState(""); - const [cycleSearch, setCycleSearch] = useState(""); - const [moduleSearch, setModuleSearch] = useState(""); + const [projectSearch, setProjectSearch] = useState(''); + const [stateSearch, setStateSearch] = useState(''); + const [assigneeSearch, setAssigneeSearch] = useState(''); + const [labelSearch, setLabelSearch] = useState(''); + const [cycleSearch, setCycleSearch] = useState(''); + const [moduleSearch, setModuleSearch] = useState(''); const [createLabelLoading, setCreateLabelLoading] = useState(false); const [createLabelError, setCreateLabelError] = useState(null); useEffect(() => { if (!openDropdown) { - setProjectSearch(""); - setStateSearch(""); - setAssigneeSearch(""); - setLabelSearch(""); - setCycleSearch(""); - setModuleSearch(""); + setProjectSearch(''); + setStateSearch(''); + setAssigneeSearch(''); + setLabelSearch(''); + setCycleSearch(''); + setModuleSearch(''); setCreateLabelError(null); } }, [openDropdown]); - const selectedProject = - projects.find((p) => p.id === projectId) ?? projects[0]; - const pid = selectedProject?.id ?? ""; + const selectedProject = projects.find((p) => p.id === projectId) ?? projects[0]; + const pid = selectedProject?.id ?? ''; const [states, setStates] = useState([]); const [labels, setLabels] = useState([]); const [issues, setIssues] = useState([]); const [cycles, setCycles] = useState>([]); - const [modules, setModules] = useState>( - [], - ); - const [members, setMembers] = useState>( - [], - ); + const [modules, setModules] = useState>([]); + const [members, setMembers] = useState>([]); useEffect(() => { if (!workspaceSlug || !pid) { @@ -312,8 +233,8 @@ export function CreateWorkItemModal({ id: m.member_id, name: (m.member_display_name && m.member_display_name.trim()) || - (m.member_email && m.member_email.split("@")[0]) || - "Member", + (m.member_email && m.member_email.split('@')[0]) || + 'Member', })), ); }) @@ -325,58 +246,42 @@ export function CreateWorkItemModal({ }; }, [workspaceSlug]); - const stateName = stateId ? states.find((s) => s.id === stateId)?.name : ""; + const stateName = stateId ? states.find((s) => s.id === stateId)?.name : ''; const assigneeNames = assigneeIds .map((id) => members.find((m) => m.id === id)?.name ?? id.slice(0, 8)) .filter(Boolean) - .join(", ") || ""; + .join(', ') || ''; const labelNames = labelIds .map((id) => labels.find((l) => l.id === id)?.name) .filter(Boolean) - .join(", ") || ""; - const cycleName = cycleId ? cycles.find((c) => c.id === cycleId)?.name : ""; - const moduleName = moduleId - ? modules.find((m) => m.id === moduleId)?.name - : ""; - const parentTitle = parentId - ? issues.find((i) => i.id === parentId)?.name - : ""; + .join(', ') || ''; + const cycleName = cycleId ? cycles.find((c) => c.id === cycleId)?.name : ''; + const moduleName = moduleId ? modules.find((m) => m.id === moduleId)?.name : ''; + const parentTitle = parentId ? issues.find((i) => i.id === parentId)?.name : ''; const q = (s: string) => s.toLowerCase().trim(); - const filteredProjects = projects.filter((p) => - q(p.name).includes(q(projectSearch)), - ); - const filteredStates = states.filter((s) => - q(s.name).includes(q(stateSearch)), - ); + const filteredProjects = projects.filter((p) => q(p.name).includes(q(projectSearch))); + const filteredStates = states.filter((s) => q(s.name).includes(q(stateSearch))); const filteredUsers = members.filter( - (u) => - q(u.name).includes(q(assigneeSearch)) || - q(u.id).includes(q(assigneeSearch)), - ); - const filteredLabels = labels.filter((l) => - q(l.name).includes(q(labelSearch)), - ); - const filteredCycles = cycles.filter((c) => - q(c.name).includes(q(cycleSearch)), - ); - const filteredModules = modules.filter((m) => - q(m.name).includes(q(moduleSearch)), + (u) => q(u.name).includes(q(assigneeSearch)) || q(u.id).includes(q(assigneeSearch)), ); + const filteredLabels = labels.filter((l) => q(l.name).includes(q(labelSearch))); + const filteredCycles = cycles.filter((c) => q(c.name).includes(q(cycleSearch))); + const filteredModules = modules.filter((m) => q(m.name).includes(q(moduleSearch))); useEffect(() => { if (open) { - setProjectId(defaultProjectId ?? projects[0]?.id ?? ""); - setTitle(""); - setDescription(""); - setStateId(""); - setPriority("none"); + setProjectId(defaultProjectId ?? projects[0]?.id ?? ''); + setTitle(''); + setDescription(''); + setStateId(''); + setPriority('none'); setAssigneeIds([]); setLabelIds([]); - setStartDate(""); - setDueDate(""); + setStartDate(''); + setDueDate(''); setCycleId(null); setModuleId(defaultModuleId ?? null); setParentId(null); @@ -388,17 +293,17 @@ export function CreateWorkItemModal({ useEffect(() => { if (!open) return; const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { if (parentModalOpen) setParentModalOpen(false); else if (openDropdown) setOpenDropdown(null); else onClose(); } }; - document.addEventListener("keydown", handleEscape); - document.body.style.overflow = "hidden"; + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; return () => { - document.removeEventListener("keydown", handleEscape); - document.body.style.overflow = ""; + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = ''; }; }, [open, onClose, openDropdown, parentModalOpen]); @@ -412,7 +317,7 @@ export function CreateWorkItemModal({ description, projectId, stateId: stateId || undefined, - priority: priority !== "none" ? priority : undefined, + priority: priority !== 'none' ? priority : undefined, assigneeIds: assigneeIds.length ? assigneeIds : undefined, assigneeId: assigneeIds[0] ?? undefined, labelIds: labelIds.length ? labelIds : undefined, @@ -424,14 +329,14 @@ export function CreateWorkItemModal({ }); if (!createMore) onClose(); else { - setTitle(""); - setDescription(""); - setStateId(""); - setPriority("none"); + setTitle(''); + setDescription(''); + setStateId(''); + setPriority('none'); setAssigneeIds([]); setLabelIds([]); - setStartDate(""); - setDueDate(""); + setStartDate(''); + setDueDate(''); setCycleId(null); setModuleId(null); setParentId(null); @@ -442,14 +347,14 @@ export function CreateWorkItemModal({ } else { if (!createMore) onClose(); else { - setTitle(""); - setDescription(""); - setStateId(""); - setPriority("none"); + setTitle(''); + setDescription(''); + setStateId(''); + setPriority('none'); setAssigneeIds([]); setLabelIds([]); - setStartDate(""); - setDueDate(""); + setStartDate(''); + setDueDate(''); setCycleId(null); setModuleId(null); setParentId(null); @@ -458,15 +363,13 @@ export function CreateWorkItemModal({ }; const handleDiscard = () => { - setTitle(""); - setDescription(""); + setTitle(''); + setDescription(''); onClose(); }; const toggleLabel = (id: string) => { - setLabelIds((prev) => - prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], - ); + setLabelIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])); }; const handleCreateLabel = async () => { @@ -478,12 +381,10 @@ export function CreateWorkItemModal({ const created = await labelService.create(workspaceSlug, pid, { name }); setLabels((prev) => [...prev, created]); setLabelIds((prev) => [...prev, created.id]); - setLabelSearch(""); + setLabelSearch(''); setOpenDropdown(null); } catch (err) { - setCreateLabelError( - err instanceof Error ? err.message : "Failed to create label.", - ); + setCreateLabelError(err instanceof Error ? err.message : 'Failed to create label.'); } finally { setCreateLabelLoading(false); } @@ -499,20 +400,13 @@ export function CreateWorkItemModal({ aria-modal="true" aria-labelledby="create-work-item-title" > -
+
e.stopPropagation()} >
-

+

Create new work item

@@ -522,13 +416,9 @@ export function CreateWorkItemModal({ onOpen={setOpenDropdown} label="Select project" icon={ - selectedProject?.name.includes("Logistics") ? ( - - ) : ( - - ) + selectedProject?.name.includes('Logistics') ? : } - displayValue={selectedProject?.name ?? ""} + displayValue={selectedProject?.name ?? ''} panelClassName="flex min-w-[160px] max-h-52 flex-col rounded border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)" >
@@ -580,7 +470,7 @@ export function CreateWorkItemModal({ onOpen={setOpenDropdown} label="Backlog" icon={} - displayValue={stateName || "Backlog"} + displayValue={stateName || 'Backlog'} compact panelClassName="flex min-w-[120px] max-h-52 flex-col rounded border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)" > @@ -616,8 +506,8 @@ export function CreateWorkItemModal({ label="None" icon={} displayValue={ - priority === "none" - ? "None" + priority === 'none' + ? 'None' : priority.charAt(0).toUpperCase() + priority.slice(1) } compact @@ -643,7 +533,7 @@ export function CreateWorkItemModal({ onOpen={setOpenDropdown} label="Assignees" icon={} - displayValue={assigneeNames || "Add assignees"} + displayValue={assigneeNames || 'Add assignees'} compact panelClassName="flex min-w-[120px] max-h-52 flex-col rounded border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)" > @@ -673,14 +563,12 @@ export function CreateWorkItemModal({ type="button" onClick={() => setAssigneeIds((prev) => - prev.includes(u.id) - ? prev.filter((x) => x !== u.id) - : [...prev, u.id], + prev.includes(u.id) ? prev.filter((x) => x !== u.id) : [...prev, u.id], ) } className="w-full text-left text-(--txt-primary) hover:bg-(--bg-layer-1-hover)" > - {u.name} {assigneeIds.includes(u.id) ? "✓" : ""} + {u.name} {assigneeIds.includes(u.id) ? '✓' : ''} ))}
@@ -719,9 +607,7 @@ export function CreateWorkItemModal({ ))} {labelSearch.trim() && !labels.some( - (l) => - l.name.toLowerCase() === - labelSearch.trim().toLowerCase(), + (l) => l.name.toLowerCase() === labelSearch.trim().toLowerCase(), ) && ( <>
@@ -732,13 +618,11 @@ export function CreateWorkItemModal({ className="w-full text-left text-(--brand-default) hover:bg-(--bg-layer-1-hover) disabled:opacity-50" > {createLabelLoading - ? "Creating…" + ? 'Creating…' : `Create label "${labelSearch.trim()}"`} {createLabelError && ( -

- {createLabelError} -

+

{createLabelError}

)} )} @@ -764,7 +648,7 @@ export function CreateWorkItemModal({ onOpen={setOpenDropdown} label="Cycle" icon={} - displayValue={cycleName || "No cycle"} + displayValue={cycleName || 'No cycle'} compact panelClassName="flex min-w-[120px] max-h-52 flex-col rounded border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)" > @@ -809,7 +693,7 @@ export function CreateWorkItemModal({ onOpen={setOpenDropdown} label="Modules" icon={} - displayValue={moduleName ?? ""} + displayValue={moduleName ?? ''} compact panelClassName="flex min-w-[120px] max-h-52 flex-col rounded border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)" > @@ -856,7 +740,7 @@ export function CreateWorkItemModal({ - {parentTitle || "Add parent"} + {parentTitle || 'Add parent'}
@@ -874,26 +758,14 @@ export function CreateWorkItemModal({ Create more - {createError && ( -

- {createError} -

- )} + {createError &&

{createError}

}
- -
diff --git a/ui/src/components/ProjectIconModal.tsx b/ui/src/components/ProjectIconModal.tsx index ece57f00..8ff45cd3 100644 --- a/ui/src/components/ProjectIconModal.tsx +++ b/ui/src/components/ProjectIconModal.tsx @@ -1,96 +1,84 @@ -import { useState, useMemo, useEffect } from "react"; -import { Modal } from "./ui"; +import { useState, useMemo, useEffect } from 'react'; +import { Modal } from './ui'; const EMOJI_LIST = [ - "🏠", - "🚀", - "📁", - "💡", - "🔧", - "⭐", - "🎯", - "📌", - "🏷️", - "📋", - "✅", - "🐛", - "🔒", - "📱", - "💻", - "🌟", - "🎨", - "📊", - "📈", - "🔥", - "❤️", - "🎉", - "📦", - "🔔", - "⚙️", - "🌈", - "🏆", - "📝", - "🗂️", - "🔑", - "🎪", - "🧩", - "📌", - "🛠️", - "💼", - "🌍", - "🔔", + '🏠', + '🚀', + '📁', + '💡', + '🔧', + '⭐', + '🎯', + '📌', + '🏷️', + '📋', + '✅', + '🐛', + '🔒', + '📱', + '💻', + '🌟', + '🎨', + '📊', + '📈', + '🔥', + '❤️', + '🎉', + '📦', + '🔔', + '⚙️', + '🌈', + '🏆', + '📝', + '🗂️', + '🔑', + '🎪', + '🧩', + '📌', + '🛠️', + '💼', + '🌍', + '🔔', ]; const ICON_COLORS = [ - "#94a3b8", - "#64748b", - "#6366f1", - "#3b82f6", - "#0ea5e9", - "#22c55e", - "#eab308", - "#f97316", - "#ec4899", - "#ffffff", + '#94a3b8', + '#64748b', + '#6366f1', + '#3b82f6', + '#0ea5e9', + '#22c55e', + '#eab308', + '#f97316', + '#ec4899', + '#ffffff', ]; // Simple line icons (name -> path d) const ICON_PATHS: Record = { - home: "m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", - star: "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z", - folder: - "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z", - briefcase: "M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16", - zap: "M13 2 3 14h9l-1 8 10-12h-9l1-8z", - target: - "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z", - flag: "M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z", - bookmark: "m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z", - box: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z", - globe: - "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z", - cog: "M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", + home: 'm3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z', + star: 'm12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', + folder: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z', + briefcase: 'M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16', + zap: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z', + target: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z', + flag: 'M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z', + bookmark: 'm19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z', + box: 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z', + globe: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z', + cog: 'M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', heart: - "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z", + 'M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z', rocket: - "M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z", + 'M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z', lightbulb: - "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5", - palette: - "M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z", + 'M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5', + palette: 'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z', }; const ICON_NAMES = Object.keys(ICON_PATHS); -function IconSvg({ - name, - color, - size = 24, -}: { - name: string; - color?: string; - size?: number; -}) { +function IconSvg({ name, color, size = 24 }: { name: string; color?: string; size?: number }) { const d = ICON_PATHS[name]; if (!d) return null; return ( @@ -99,7 +87,7 @@ function IconSvg({ height={size} viewBox="0 0 24 24" fill="none" - stroke={color || "currentColor"} + stroke={color || 'currentColor'} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" @@ -115,7 +103,7 @@ export function ProjectIconDisplay({ emoji, icon_prop, size = 20, - className = "", + className = '', }: { emoji?: string | null; icon_prop?: { name?: string; color?: string } | null; @@ -124,12 +112,7 @@ export function ProjectIconDisplay({ }) { if (emoji) { return ( - + {emoji} ); @@ -137,11 +120,7 @@ export function ProjectIconDisplay({ if (icon_prop?.name && ICON_PATHS[icon_prop.name]) { return ( - + ); } @@ -166,31 +145,29 @@ export interface ProjectIconModalProps { currentIconProp?: { name?: string; color?: string } | null; } -const TAB_EMOJI = "emoji"; -const TAB_ICON = "icon"; +const TAB_EMOJI = 'emoji'; +const TAB_ICON = 'icon'; type Tab = typeof TAB_EMOJI | typeof TAB_ICON; export function ProjectIconModal({ open, onClose, onSelect, - title = "Project icon", + title = 'Project icon', currentEmoji, currentIconProp, }: ProjectIconModalProps) { void currentEmoji; // reserved for future use (e.g. pre-select emoji tab) const [tab, setTab] = useState(TAB_EMOJI); - const [emojiSearch, setEmojiSearch] = useState(""); - const [iconColor, setIconColor] = useState( - currentIconProp?.color ?? "#6366f1", - ); + const [emojiSearch, setEmojiSearch] = useState(''); + const [iconColor, setIconColor] = useState(currentIconProp?.color ?? '#6366f1'); useEffect(() => { if (open) { // Intentional: sync form state when modal opens (kept for future use) // eslint-disable-next-line react-hooks/set-state-in-effect - setIconColor(currentIconProp?.color ?? "#6366f1"); - setEmojiSearch(""); + setIconColor(currentIconProp?.color ?? '#6366f1'); + setEmojiSearch(''); } }, [open, currentIconProp?.color]); @@ -198,9 +175,7 @@ export function ProjectIconModal({ if (!emojiSearch.trim()) return EMOJI_LIST; const q = emojiSearch.toLowerCase(); return EMOJI_LIST.filter( - (e) => - e.includes(q) || - String.fromCodePoint(e.codePointAt(0)!).toLowerCase().includes(q), + (e) => e.includes(q) || String.fromCodePoint(e.codePointAt(0)!).toLowerCase().includes(q), ); }, [emojiSearch]); @@ -225,21 +200,15 @@ export function ProjectIconModal({ ); return ( - +
- } > - {error && ( -

{error}

- )} + {error &&

{error}

}
{s.label} - {s.id === status && ( - - )} + {s.id === status && } ))}
@@ -271,29 +247,21 @@ export function UpdateModuleModal({ className="flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-left text-sm text-(--txt-primary) hover:bg-(--bg-layer-1-hover)" > No lead - {leadId === null && ( - - )} + {leadId === null && } {filteredLead.map((m) => ( )} @@ -103,19 +93,13 @@ export function UploadImageModal({ Cancel ); return ( - +
{!preview ? (
-

- Drag & drop image here -

+

Drag & drop image here

) : (
- Preview + Preview
); diff --git a/ui/src/components/instance-admin/CreateWorkspaceSetupHint.tsx b/ui/src/components/instance-admin/CreateWorkspaceSetupHint.tsx index 16ed394e..7bdac46d 100644 --- a/ui/src/components/instance-admin/CreateWorkspaceSetupHint.tsx +++ b/ui/src/components/instance-admin/CreateWorkspaceSetupHint.tsx @@ -1,4 +1,4 @@ -import { Button } from "../ui"; +import { Button } from '../ui'; const IconWorkspace = () => ( void; } -export function CreateWorkspaceSetupHint({ - onDismiss, -}: CreateWorkspaceSetupHintProps) { +export function CreateWorkspaceSetupHint({ onDismiss }: CreateWorkspaceSetupHintProps) { return (
-

- Create workspace -

+

Create workspace

- Instance setup is complete. Welcome to your Devlane instance. Start - your journey by creating your first workspace. + Instance setup is complete. Welcome to your Devlane instance. Start your journey by + creating your first workspace.

-
diff --git a/ui/src/components/instance-admin/index.ts b/ui/src/components/instance-admin/index.ts index b05320c6..92031860 100644 --- a/ui/src/components/instance-admin/index.ts +++ b/ui/src/components/instance-admin/index.ts @@ -1 +1 @@ -export { CreateWorkspaceSetupHint } from "./CreateWorkspaceSetupHint"; +export { CreateWorkspaceSetupHint } from './CreateWorkspaceSetupHint'; diff --git a/ui/src/components/layout/AppShell.tsx b/ui/src/components/layout/AppShell.tsx index b19636f0..a8223dc1 100644 --- a/ui/src/components/layout/AppShell.tsx +++ b/ui/src/components/layout/AppShell.tsx @@ -1,13 +1,13 @@ -import { Outlet, useLocation } from "react-router-dom"; -import { ModulesFilterProvider } from "../../contexts/ModulesFilterContext"; -import { ProjectSavedViewDisplayProvider } from "../../contexts/ProjectSavedViewDisplayContext"; -import { WorkspaceViewsStateProvider } from "../../contexts/WorkspaceViewsStateContext"; -import { PageHeader } from "./PageHeader"; -import { Sidebar } from "./Sidebar"; +import { Outlet, useLocation } from 'react-router-dom'; +import { ModulesFilterProvider } from '../../contexts/ModulesFilterContext'; +import { ProjectSavedViewDisplayProvider } from '../../contexts/ProjectSavedViewDisplayContext'; +import { WorkspaceViewsStateProvider } from '../../contexts/WorkspaceViewsStateContext'; +import { PageHeader } from './PageHeader'; +import { Sidebar } from './Sidebar'; export function AppShell() { const { pathname } = useLocation(); - const isViewsRoute = pathname.includes("/views"); + const isViewsRoute = pathname.includes('/views'); return ( @@ -19,7 +19,7 @@ export function AppShell() {
diff --git a/ui/src/components/layout/Header.tsx b/ui/src/components/layout/Header.tsx index 8ae0686c..03491cba 100644 --- a/ui/src/components/layout/Header.tsx +++ b/ui/src/components/layout/Header.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState } from "react"; -import { Link, useParams } from "react-router-dom"; -import { Button } from "../ui"; -import { useAuth } from "../../contexts/AuthContext"; -import { workspaceService } from "../../services/workspaceService"; -import { projectService } from "../../services/projectService"; -import type { WorkspaceApiResponse, ProjectApiResponse } from "../../api/types"; +import { useEffect, useState } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { Button } from '../ui'; +import { useAuth } from '../../contexts/AuthContext'; +import { workspaceService } from '../../services/workspaceService'; +import { projectService } from '../../services/projectService'; +import type { WorkspaceApiResponse, ProjectApiResponse } from '../../api/types'; export function Header() { const { user, logout } = useAuth(); @@ -67,15 +67,12 @@ export function Header() { >
Devlane -