Merge pull request #236 from donetick/0815-fixes

0815 fixes
This commit is contained in:
Mohamad Tarbin
2026-08-17 01:03:36 -04:00
committed by GitHub
19 changed files with 380 additions and 233 deletions

View File

@@ -13,3 +13,4 @@ files:
- source: /public/locales/en/*.json
translation: /public/locales/%two_letters_code%/%original_file_name%
update_option: update_as_unapproved
skip_untranslated_strings: true

68
package-lock.json generated
View File

@@ -42,6 +42,7 @@
"@mui/joy": "5.0.0-beta.52",
"@mui/material": "^5.15.2",
"@openreplay/tracker": "^14.0.4",
"@posthog/rollup-plugin": "^1.4.9",
"@revenuecat/purchases-capacitor": "^12.0.0",
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
"@stylistic/eslint-plugin": "^5.10.0",
@@ -4596,6 +4597,49 @@
"@posthog/types": "^1.402.2"
}
},
"node_modules/@posthog/cli": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/@posthog/cli/-/cli-0.11.2.tgz",
"integrity": "sha512-iqYIl/Q2FfE/+vftlHqGYFz4DRcDon7t28PU6kIefFFUMr0Z4bZrypnxU/Wl4xW/0uUw4fiAKs2tkWmwh3lSDA==",
"hasInstallScript": true,
"hasShrinkwrap": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.1.2"
},
"bin": {
"posthog-cli": "run-posthog-cli.js"
},
"engines": {
"node": ">=14.14",
"npm": ">=6"
}
},
"node_modules/@posthog/cli/node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/@posthog/cli/node_modules/prettier": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"extraneous": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/@posthog/core": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.47.0.tgz",
@@ -4605,6 +4649,29 @@
"@posthog/types": "^1.402.2"
}
},
"node_modules/@posthog/plugin-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@posthog/plugin-utils/-/plugin-utils-1.1.3.tgz",
"integrity": "sha512-bJ7llB5We3NRB6VZfPLAn71Ps8+Q/bEP3T4/Q61CDE8tbkSMiVvWp9cyGRBdBmPnX4X5rT+rEtUZreVPwTvw3g==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/@posthog/rollup-plugin": {
"version": "1.4.9",
"resolved": "https://registry.npmjs.org/@posthog/rollup-plugin/-/rollup-plugin-1.4.9.tgz",
"integrity": "sha512-OzmX630yoyp5+cIeqlr0lLRW4P/ppabK9QC/pe+lqArCohYKp5gjl5fKgYlQ3ruc6Hm+JyF0KuS160O1ZHAvzw==",
"license": "MIT",
"dependencies": {
"@posthog/cli": "~0.11.1",
"@posthog/plugin-utils": "^1.1.3",
"magic-string": "^0.30.17"
},
"peerDependencies": {
"rollup": ">= 4.0.0"
}
},
"node_modules/@posthog/types": {
"version": "1.402.3",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.402.3.tgz",
@@ -8010,7 +8077,6 @@
},
"node_modules/detect-libc": {
"version": "2.0.4",
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"

View File

@@ -74,6 +74,7 @@
"@mui/joy": "5.0.0-beta.52",
"@mui/material": "^5.15.2",
"@openreplay/tracker": "^14.0.4",
"@posthog/rollup-plugin": "^1.4.9",
"@revenuecat/purchases-capacitor": "^12.0.0",
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
"@stylistic/eslint-plugin": "^5.10.0",

View File

@@ -30,7 +30,7 @@
"activities": "Activities",
"points": "Points",
"settings": "Settings",
"reportBug": "Report a Bug"
"reportBug": "Report an Issue"
},
"search": {
"title": "Search",
@@ -41,7 +41,8 @@
"recent": "Recent",
"empty": {
"title": "No direct matches",
"subtitle": "You can still filter the task list with this search."
"subtitle": "You can still filter the task list with this search.",
"actionsSubtitle": "No view or action matches. Press backspace to search everything."
},
"groups": {
"tasks": "Tasks",
@@ -76,7 +77,14 @@
"open": "Open",
"results_one": "{{count}} result",
"results_other": "{{count}} results",
"typeToSearch": "Type to search"
"typeToSearch": "Type to search",
"slashHint": "/ to jump to a view"
},
"modes": {
"actions": {
"label": "Go to",
"placeholder": "Jump to a view or action"
}
}
},
"policyUpdate": {

View File

@@ -91,8 +91,8 @@
"description": "Tell us how Donetick is working for you or request a feature."
},
"bugReport": {
"title": "Report a Bug",
"description": "Something not working right? Send us the details along with a technical snapshot."
"title": "Report an Issue",
"description": "Tell us what's not working and we'll attach the technical details for you."
}
}
},

View File

@@ -2,7 +2,8 @@ import { Preferences } from '@capacitor/preferences'
// Two independent consent axes, matching the existing onboarding UI
// (HeardAboutView's PrivacyPreferences): "analytics" gates track(), "crash"
// gates captureError(). A self-hosted user can opt into one without the other.
// gates captureException(). A self-hosted user can opt into one without the
// other.
const CONSENT_KEYS = {
analytics: 'analytics_consent',
crash: 'analytics_crash_consent',

View File

@@ -79,12 +79,6 @@ export const EVENT_SCHEMAS = {
}
export const ERROR_SCHEMAS = {
api_error: {
http_status: 'string',
method: 'string',
error_code: 'string',
operation: 'string',
},
// No message/stack field here by design — those come from the real Error
// object passed to posthog.captureException() itself, not from this
// sanitized properties bag. This schema only classifies how it was caught.

View File

@@ -142,20 +142,6 @@ export const track = (eventName, properties = {}) => {
posthog.capture(eventName, sanitized)
}
/** Backend/API failures: a normal sanitized event, same as track() — not
* PostHog's Error Tracking product. There's no real Error object here (just
* an HTTP response), so there's no stack trace to gain from captureException. */
export const captureError = (errorType, properties = {}) => {
if (!canSend('crash')) return
const posthog = getClientSync()
if (!posthog) return
const sanitized = sanitizeErrorProperties(errorType, properties)
if (!sanitized) return
posthog.capture(errorType, sanitized)
}
/**
* Frontend crashes only. Uses captureException (not capture) so these land
* on PostHog's Error Tracking page with a genuine message + stack trace —
@@ -176,7 +162,7 @@ export const captureException = (error, properties = {}) => {
let globalHandlersInstalled = false
/** Reports uncaught exceptions and unhandled promise rejections to
* PostHog's Error Tracking, gated by the same crash consent as api_error.
* PostHog's Error Tracking, gated by crash consent.
* Complements, doesn't overlap with, src/views/Error.jsx: that's a React
* Router error-boundary screen for render/loader errors, which React catches
* before they ever reach window.onerror — a different class of failure, with
@@ -206,7 +192,7 @@ export const installGlobalErrorHandlers = () => {
/**
* kind: 'analytics' | 'crash'. Enabling analytics (re-)initializes PostHog
* if needed and sends analytics_enabled; enabling crash-only never talks to
* PostHog by itself (it only unlocks captureError once something reports).
* PostHog by itself (it only unlocks captureException once something crashes).
* Disabling never sends an event and clears identity/queued data.
*/
export const setConsent = async (kind, value, { source } = {}) => {
@@ -236,7 +222,7 @@ export const setConsent = async (kind, value, { source } = {}) => {
} else if (kind === 'crash') {
// Crash reporting alone doesn't need PostHog started with the analytics
// super-properties path, but it does need a live client + identity to
// send captureError() calls through.
// send captureException() calls through.
if (isConfigured() && !getClientSync()) {
await startPosthog()
}

View File

@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { setServerVersion } from '../service/DiagnosticsSession'
import { GetResource } from '../utils/Fetcher'
// Helper to check if we have a valid token
@@ -18,6 +19,9 @@ export const useResource = () => {
queryKey: ['resource'],
queryFn: async () => {
const response = await GetResource()
// The backend only names its build here, so this is also where crash
// reports learn which server version the user was talking to.
setServerVersion(response?.api_version, response?.api_commit)
return response
},
staleTime: 6 * 60 * 60 * 1000, // 6 hours in milliseconds

View File

@@ -162,6 +162,19 @@ const buildQuickActions = t => [
})),
]
// Typing "/" as the first character switches the palette into navigation mode,
// borrowing the slash-menu reflex from Notion and Slack. The sigil itself is
// stripped from the visible query so the mode reads as state (the chip in the
// input) rather than as syntax the person has to keep typing around.
const MODE_SIGIL = '/'
const parseMode = value => {
const text = value || ''
return text.startsWith(MODE_SIGIL)
? { mode: 'actions', term: text.slice(MODE_SIGIL.length).trimStart() }
: { mode: null, term: text }
}
const readRecents = () => {
try {
return JSON.parse(localStorage.getItem(RECENTS_KEY)) || []
@@ -268,7 +281,8 @@ const GlobalSearchPalette = ({
const focusInputRef = useCallback(node => {
if (node) requestAnimationFrame(() => node.focus())
}, [])
const [query, setQuery] = useState(initialQuery || '')
const [query, setQuery] = useState(() => parseMode(initialQuery).term)
const [mode, setMode] = useState(() => parseMode(initialQuery).mode)
const [selectedIndex, setSelectedIndex] = useState(0)
const [recents] = useState(readRecents)
const selectedResultRef = useRef(null)
@@ -317,8 +331,34 @@ const GlobalSearchPalette = ({
[documents],
)
const searchActions = useCallback(
normalized => {
const matches = (quickActionIndex.search(normalized, { limit: 8 }) || [])
.map(match => ({ ...match.item, score: match.score ?? 1 }))
.sort((a, b) => a.score - b.score)
// An action whose title the query starts spelling out ("create la…") is
// what the person is after, so it leads. Anything matched only through
// its keywords stays below.
const leading = matches.filter(action =>
action.title.toLocaleLowerCase().startsWith(normalized),
)
return [
...leading,
...matches.filter(action => !leading.includes(action)),
]
},
[quickActionIndex],
)
const results = useMemo(() => {
const normalized = query.trim().toLocaleLowerCase()
// Navigation mode answers only with actions — an empty term lists them all,
// which is how the sigil teaches itself the first time someone hits "/".
if (mode === 'actions') {
return normalized ? searchActions(normalized) : quickActions
}
if (!normalized) {
const currentById = new Map(documents.map(item => [item.id, item]))
const recentResults = recents
@@ -347,15 +387,9 @@ const GlobalSearchPalette = ({
})
.sort((a, b) => a.score - b.score),
)
const actionMatches = (
quickActionIndex.search(normalized, { limit: 4 }) || []
)
.map(match => ({ ...match.item, score: match.score ?? 1 }))
.sort((a, b) => a.score - b.score)
// An action whose title the query starts spelling out ("create la…") is
// what the person is after, so it leads. Anything matched only through its
// keywords stays below the real content it shares words with.
// Keyword-only action matches stay below the real content they share words
// with; title-prefix matches lead.
const actionMatches = searchActions(normalized).slice(0, 4)
const leadingActions = actionMatches.filter(action =>
action.title.toLocaleLowerCase().startsWith(normalized),
)
@@ -375,7 +409,16 @@ const GlobalSearchPalette = ({
route: `/chores?search=${encodeURIComponent(query.trim())}`,
},
]
}, [documents, query, quickActionIndex, recents, searchIndexes, t])
}, [
documents,
mode,
query,
quickActions,
recents,
searchActions,
searchIndexes,
t,
])
// Everything except the always-present "filter the task list" fallback.
const matchCount = results.filter(
@@ -395,8 +438,25 @@ const GlobalSearchPalette = ({
if (presentation === 'modal') onClose()
}
const onQueryChange = value => {
setSelectedIndex(0)
if (!mode) {
const parsed = parseMode(value)
setMode(parsed.mode)
setQuery(parsed.term)
return
}
setQuery(value)
}
const onInputKeyDown = event => {
if (event.key === 'ArrowDown') {
// Backspace on an empty term steps back out of the mode, so the sigil is
// one keystroke to enter and one to leave.
if (event.key === 'Backspace' && mode && !query) {
event.preventDefault()
setMode(null)
setSelectedIndex(0)
} else if (event.key === 'ArrowDown') {
event.preventDefault()
setSelectedIndex(index => Math.min(index + 1, results.length - 1))
} else if (event.key === 'ArrowUp') {
@@ -423,13 +483,30 @@ const GlobalSearchPalette = ({
},
}}
value={query}
onChange={event => {
setQuery(event.target.value)
setSelectedIndex(0)
}}
onChange={event => onQueryChange(event.target.value)}
onKeyDown={onInputKeyDown}
placeholder={t('search.placeholder')}
startDecorator={<SearchRounded />}
placeholder={
mode
? t('search.modes.actions.placeholder')
: t('search.placeholder')
}
startDecorator={
mode ? (
<Chip
size='sm'
variant='soft'
color='primary'
onClick={() => {
setMode(null)
setSelectedIndex(0)
}}
>
{t('search.modes.actions.label')}
</Chip>
) : (
<SearchRounded />
)
}
endDecorator={
isLoading ? (
<CircularProgress size='sm' />
@@ -468,7 +545,9 @@ const GlobalSearchPalette = ({
/>
<Typography level='title-md'>{t('search.empty.title')}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{t('search.empty.subtitle')}
{mode
? t('search.empty.actionsSubtitle')
: t('search.empty.subtitle')}
</Typography>
</Box>
)}
@@ -554,8 +633,13 @@ const GlobalSearchPalette = ({
{t('search.footer.navigate')}
</Typography>
<Typography level='body-xs'> {t('search.footer.open')}</Typography>
{!mode && (
<Typography level='body-xs'>
{t('search.footer.slashHint')}
</Typography>
)}
<Typography level='body-xs' sx={{ ml: 'auto' }}>
{query.trim()
{query.trim() || mode
? t('search.footer.results', { count: matchCount })
: t('search.footer.typeToSearch')}
</Typography>

View File

@@ -5,7 +5,8 @@
*
* Deliberately dependency-free — ApiClient imports it on the request path, so
* anything imported here would risk a module cycle. Everything is in memory
* and dies with the tab; nothing is persisted.
* and dies with the tab, except the server build, which is remembered across
* launches so a crash before the first API answer still names the backend.
*/
const SESSION_STARTED_AT = Date.now()
@@ -25,6 +26,7 @@ const routeTrail = []
const apiFailures = []
let backgroundedCount = 0
let serverVersion = null
let serverCommit = null
// ---------------------------------------------------------------------------
// Route trail
@@ -67,6 +69,33 @@ export const getPreviousRoute = () =>
// Server identity
// ---------------------------------------------------------------------------
const SERVER_BUILD_KEY = 'diagnostics_server_build'
// A crash on cold start happens before /resource has answered, and that is
// exactly when knowing which backend the user is on matters most. Carrying the
// last known build across launches keeps the report from saying "not reported".
try {
const cached = JSON.parse(localStorage.getItem(SERVER_BUILD_KEY) || 'null')
serverVersion = cached?.version ?? null
serverCommit = cached?.commit ?? null
} catch {
// corrupt or unavailable storage just means we start without a known build
}
const rememberServerBuild = (version, commit) => {
if (!version && !commit) return
serverVersion = version || serverVersion
serverCommit = commit || serverCommit
try {
localStorage.setItem(
SERVER_BUILD_KEY,
JSON.stringify({ version: serverVersion, commit: serverCommit }),
)
} catch {
// storage full or blocked; the in-memory copy still serves this session
}
}
/**
* Picks the server build out of response headers. Costs nothing when the
* server doesn't send them — the field simply stays null.
@@ -74,21 +103,24 @@ export const getPreviousRoute = () =>
export const recordServerVersionFromResponse = response => {
if (serverVersion) return
try {
serverVersion =
rememberServerBuild(
response?.headers?.get?.('x-donetick-version') ||
response?.headers?.get?.('x-api-version') ||
null
response?.headers?.get?.('x-api-version'),
null,
)
} catch {
// headers may be inaccessible on opaque responses; not worth reporting
}
}
export const setServerVersion = version => {
if (version) serverVersion = version
}
/** Authoritative source: what /resource reports about the backend build. */
export const setServerVersion = (version, commit) =>
rememberServerBuild(version, commit)
export const getServerVersion = () => serverVersion
export const getServerCommit = () => serverCommit
// ---------------------------------------------------------------------------
// API failures
// ---------------------------------------------------------------------------
@@ -176,6 +208,7 @@ export const getSessionDiagnostics = async () => {
navigationType: NAVIGATION_TYPE,
backgroundedCount,
serverVersion,
serverCommit,
previousRoute: getPreviousRoute(),
routeTrail: getRouteTrail(),
apiFailures: getApiFailures(),

View File

@@ -153,7 +153,9 @@ export const formatErrorReport = report => {
session.previousRoute ? `Came from: ${session.previousRoute}` : null,
'',
`App: ${app.appVersion} · ${app.platform}${app.isNative ? ' (native)' : ''}`,
`Server: ${session.serverVersion ?? 'not reported'}`,
`Server: ${session.serverVersion ?? 'not reported'}${
session.serverCommit ? ` (${session.serverCommit.slice(0, 8)})` : ''
}`,
`Session: ${formatDuration(session.sessionDurationMs)} active · ${
session.navigationType ?? 'unknown'
} start · backgrounded ${session.backgroundedCount ?? 0}×`,

View File

@@ -1,10 +1,8 @@
import { Preferences } from '@capacitor/preferences'
import { captureError } from '../analytics'
import { API_URL } from '../Config'
import { networkManager } from '../hooks/NetworkManager'
import {
normalizeEndpoint,
recordApiFailure,
recordServerVersionFromResponse,
} from '../service/DiagnosticsSession'
@@ -230,11 +228,6 @@ class ApiClient {
method: config.method,
status: response.status,
})
captureError('api_error', {
http_status: String(response.status),
method: config.method || 'GET',
operation: normalizeEndpoint(endpoint),
})
}
// 2. Check for 401 (Unauthorized)
@@ -319,11 +312,6 @@ class ApiClient {
if (!externalAbort) {
networkManager.setServerUnreachable()
recordApiFailure({ endpoint, method: config.method, status: 'network' })
captureError('api_error', {
http_status: 'network',
method: config.method || 'GET',
operation: normalizeEndpoint(endpoint),
})
}
console.error('Request failed', error)
throw error

View File

@@ -11,8 +11,6 @@ import {
SearchOff,
SelectAll,
Unarchive,
ViewAgenda,
ViewModule,
} from '@mui/icons-material'
import {
Badge,
@@ -48,9 +46,7 @@ import { isOfflineFeatureEnabled } from '../../utils/OfflineFeatureToggle'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import ChoreCard from './ChoreCard'
import ChoreListView from './ChoreListView.jsx'
import CompactChoreCard from './CompactChoreCard'
import MultiSelectHelp from './MultiSelectHelp'
const sortByUpdatedAtDesc = chores =>
@@ -108,9 +104,6 @@ const ArchivedTasks = () => {
const [searchTerm, setSearchTerm] = useState('')
const [performers, setPerformers] = useState([])
const navigate = useNavigate()
const [viewMode, setViewMode] = useState(
localStorage.getItem('archivedChoreCardViewMode') || 'default',
)
const [isLoading, setIsLoading] = useState(true)
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const searchInputRef = useRef(null)
@@ -373,15 +366,6 @@ const ArchivedTasks = () => {
}
}, [isMultiSelectMode, selectedChores.size])
const toggleViewMode = () => {
const modes = ['default', 'compact']
const currentIndex = modes.indexOf(viewMode)
const nextIndex = (currentIndex + 1) % modes.length
const newMode = modes[nextIndex]
setViewMode(newMode)
localStorage.setItem('archivedChoreCardViewMode', newMode)
}
const searchOptions = {
keys: ['name', 'raw_label'],
includeScore: true,
@@ -672,26 +656,6 @@ const ArchivedTasks = () => {
})
}
// Helper function to render the appropriate card component
const renderChoreCard = (chore, key) => {
const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard
return (
<CardComponent
key={key || chore.id}
chore={chore}
onChoreUpdate={handleChoreUpdated}
onChoreRemove={handleChoreDeleted}
performers={performers}
viewOnly={false}
showActions={false}
// Multi-select props
isMultiSelectMode={isMultiSelectMode}
isSelected={selectedChores.has(chore.id)}
onSelectionToggle={() => toggleChoreSelection(chore.id)}
/>
)
}
if (isUserProfileLoading || performers.length === 0 || isLoading) {
return <LoadingComponent />
}
@@ -822,26 +786,6 @@ const ArchivedTasks = () => {
isActive={sortBy !== 'archivedAt' || sortDirection !== 'desc'}
/>
{/* View Mode Toggle Button */}
<IconButton
variant='outlined'
color='neutral'
size='sm'
sx={{
height: 32,
width: 32,
borderRadius: '50%',
}}
onClick={toggleViewMode}
title={
viewMode === 'default'
? t('archived.switchToCompact')
: t('archived.switchToCard')
}
>
{viewMode === 'default' ? <ViewAgenda /> : <ViewModule />}
</IconButton>
{/* Multi-select Toggle Button */}
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<IconButton
@@ -1137,12 +1081,12 @@ const ArchivedTasks = () => {
{searchTerm && t('archived.matching', { term: searchTerm })}
</Typography>
<List sx={{ gap: viewMode === 'compact' ? 0 : 1 }}>
<List sx={{ gap: 0 }}>
<ChoreListView
chores={finalChores}
// viewOnly={true}
showActions={false}
viewMode={viewMode}
viewMode='compact'
membersData={membersData}
isMultiSelectMode={isMultiSelectMode}
selectedChores={selectedChores}

View File

@@ -109,6 +109,8 @@ const CompactChoreCard = ({
return parts.join(' • ')
}
const showLeadingSlot = showActions || isMultiSelectMode
const showTrailingSlot = showActions && !isMultiSelectMode
return (
<Box
@@ -145,7 +147,7 @@ const CompactChoreCard = ({
}}
>
{/* Priority bar clickable area */}
{chore.priority > 0 && (
{chore.priority > 0 && onChipClick && (
<Box
sx={{
position: 'absolute',
@@ -170,10 +172,12 @@ const CompactChoreCard = ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
width: showLeadingSlot ? 40 : 0,
height: 40,
mr: 1.5,
mr: showLeadingSlot ? 1.5 : 0,
flexShrink: 0,
overflow: 'hidden',
transition: 'width 0.3s ease-in-out, margin 0.3s ease-in-out',
}}
>
{/* Complete Button */}
@@ -434,17 +438,18 @@ const CompactChoreCard = ({
role='none'
tabIndex={0}
onClick={e => {
if (!onChipClick) return
e.stopPropagation()
onChipClick({ label: l })
}}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
if (onChipClick && (e.key === 'Enter' || e.key === ' ')) {
e.stopPropagation()
onChipClick({ label: l })
}
}}
style={{
cursor: 'pointer',
cursor: onChipClick ? 'pointer' : 'inherit',
padding: 0,
margin: 0,
display: 'flex',
@@ -477,14 +482,14 @@ const CompactChoreCard = ({
sx={{
transition:
'opacity 0.3s ease-in-out, transform 0.3s ease-in-out, width 0.3s ease-in-out, margin 0.3s ease-in-out',
opacity: isMultiSelectMode ? 0 : 1,
transform: isMultiSelectMode
? 'translateX(20px) scale(0.8)'
: 'translateX(0) scale(1)',
width: isMultiSelectMode ? 0 : 32,
marginRight: isMultiSelectMode ? 0 : undefined,
opacity: showTrailingSlot ? 1 : 0,
transform: showTrailingSlot
? 'translateX(0) scale(1)'
: 'translateX(20px) scale(0.8)',
width: showTrailingSlot ? 32 : 0,
marginRight: showTrailingSlot ? undefined : 0,
overflow: 'hidden',
pointerEvents: isMultiSelectMode ? 'none' : 'auto',
pointerEvents: showTrailingSlot ? 'auto' : 'none',
}}
>
{showActions && (

View File

@@ -1,11 +1,11 @@
import { Browser } from '@capacitor/browser'
import { Capacitor } from '@capacitor/core'
import {
BugReportRounded,
CheckRounded,
ContentCopyRounded,
ExpandMoreRounded,
GitHub,
ReportProblemRounded,
} from '@mui/icons-material'
import {
Box,
@@ -165,7 +165,7 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
<Stack spacing={2}>
<Box sx={{ ...enter(0) }}>
<IconHalo
icon={<BugReportRounded />}
icon={<ReportProblemRounded />}
color={isBugReport ? 'warning' : 'danger'}
/>
</Box>
@@ -175,7 +175,7 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
level='h4'
sx={{ fontWeight: 700, letterSpacing: '-0.01em' }}
>
{isBugReport ? 'Report a bug' : 'Report this problem'}
{isBugReport ? 'Report an issue' : 'Report this problem'}
</Typography>
<Typography
level='body-sm'

View File

@@ -1,4 +1,9 @@
import { BugReport, ChevronRight, Feedback, Star } from '@mui/icons-material'
import {
ChevronRight,
Feedback,
ReportProblem,
Star,
} from '@mui/icons-material'
import {
Avatar,
Box,
@@ -52,7 +57,7 @@ const SettingsOverview = () => {
id: 'bugreport',
title: t('overview.sections.bugReport.title'),
description: t('overview.sections.bugReport.description'),
icon: <BugReport />,
icon: <ReportProblem />,
onSelect: () => setBugReportOpen(true),
},
]

View File

@@ -2,7 +2,6 @@ import { Capacitor } from '@capacitor/core'
import {
Archive,
ArrowBack,
BugReport,
FilterAlt,
FolderOpen,
History,
@@ -10,6 +9,7 @@ import {
ListAlt,
Logout,
MenuRounded,
ReportProblem,
SearchRounded,
SettingsOutlined,
Toll,
@@ -305,7 +305,7 @@ const NavBar = () => {
}}
>
<ListItemDecorator>
<BugReport />
<ReportProblem />
</ListItemDecorator>
<ListItemContent>{t('navigation.reportBug')}</ListItemContent>
</ListItemButton>

View File

@@ -1,93 +1,118 @@
import posthog from '@posthog/rollup-plugin'
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import pkg from './package.json'
// https://vitejs.dev/config/
export default defineConfig(({ command }) => ({
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
},
// Strip console.* / debugger from production bundles only, so dev logging
// is untouched. `command` is 'build' for `vite build`, 'serve' for the dev server.
esbuild: {
drop: command === 'build' ? ['console', 'debugger'] : [],
},
plugins: [
react(),
VitePWA({
includeAssets: [
'favicon.ico',
'robots.txt',
'apple-touch-icon.png',
'safari-pinned-tab.svg',
'mstile-150x150.png',
],
injectManifest: {
globIgnores: ['index.html'],
globPatterns: ['**/*.{js,css,html,png,svg}'],
},
manifest: {
background_color: '#ffffff',
display: 'standalone',
icons: [
{
sizes: '192x192',
src: '/android-chrome-192x192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: '/android-chrome-512x512.png',
type: 'image/png',
},
{
sizes: '64x64',
src: 'pwa-64x64.png',
type: 'image/png',
},
{
sizes: '192x192',
src: 'pwa-192x192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: 'pwa-512x512.png',
type: 'image/png',
},
{
purpose: 'maskable',
sizes: '512x512',
src: 'maskable-icon-512x512.png',
type: 'image/png',
},
],
name: 'Donetick: Simplify Tasks & Chores, Together.',
short_name: 'Donetick',
theme_color: '#ffffff',
},
registerType: 'prompt',
workbox: {
clientsClaim: true, // Take control of uncontrolled clients as soon as the service worker becomes active
maximumFileSizeToCacheInBytes: 6000000, // 6MB
//Exclude API and Swagger routes from service worker navigation fallback
navigateFallback: '/index.html',
navigateFallbackDenylist: [
/^\/api\//, // Exclude all API routes
/^\/swagger/, // Exclude all Swagger routes
],
skipWaiting: true, // Force the waiting service worker to become the active service worker
},
}),
],
export default ({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const personalApiKey = process.env.POSTHOG_API_KEY || env.POSTHOG_API_KEY
const projectId = process.env.POSTHOG_PROJECT_ID || env.POSTHOG_PROJECT_ID
const host = process.env.POSTHOG_HOST || env.POSTHOG_HOST
const uploadPosthogSourcemaps =
command === 'build' && Boolean(personalApiKey && projectId)
resolve: {
alias: [
{
find: '@',
replacement: '/src',
},
return defineConfig({
define: {
'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
},
// Strip console.* / debugger from production bundles only, so dev logging
// is untouched. `command` is 'build' for `vite build`, 'serve' for the dev server.
esbuild: {
drop: command === 'build' ? ['console', 'debugger'] : [],
},
plugins: [
react(),
...(uploadPosthogSourcemaps
? [
posthog({
personalApiKey,
projectId,
host,
sourcemaps: {
deleteAfterUpload: true,
},
}),
]
: []),
VitePWA({
includeAssets: [
'favicon.ico',
'robots.txt',
'apple-touch-icon.png',
'safari-pinned-tab.svg',
'mstile-150x150.png',
],
injectManifest: {
globIgnores: ['index.html'],
globPatterns: ['**/*.{js,css,html,png,svg}'],
},
manifest: {
background_color: '#ffffff',
display: 'standalone',
icons: [
{
sizes: '192x192',
src: '/android-chrome-192x192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: '/android-chrome-512x512.png',
type: 'image/png',
},
{
sizes: '64x64',
src: 'pwa-64x64.png',
type: 'image/png',
},
{
sizes: '192x192',
src: 'pwa-192x192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: 'pwa-512x512.png',
type: 'image/png',
},
{
purpose: 'maskable',
sizes: '512x512',
src: 'maskable-icon-512x512.png',
type: 'image/png',
},
],
name: 'Donetick: Simplify Tasks & Chores, Together.',
short_name: 'Donetick',
theme_color: '#ffffff',
},
registerType: 'prompt',
workbox: {
// The PWA plugin runs after PostHog's upload/cleanup stage, so prevent
// it from leaving separate service-worker source maps in dist.
sourcemap: false,
clientsClaim: true, // Take control of uncontrolled clients as soon as the service worker becomes active
maximumFileSizeToCacheInBytes: 6000000, // 6MB
//Exclude API and Swagger routes from service worker navigation fallback
navigateFallback: '/index.html',
navigateFallbackDenylist: [
/^\/api\//, // Exclude all API routes
/^\/swagger/, // Exclude all Swagger routes
],
skipWaiting: true, // Force the waiting service worker to become the active service worker
},
}),
],
},
}))
resolve: {
alias: [
{
find: '@',
replacement: '/src',
},
],
},
})
}