@@ -13,3 +13,4 @@ files:
|
|||||||
- source: /public/locales/en/*.json
|
- source: /public/locales/en/*.json
|
||||||
translation: /public/locales/%two_letters_code%/%original_file_name%
|
translation: /public/locales/%two_letters_code%/%original_file_name%
|
||||||
update_option: update_as_unapproved
|
update_option: update_as_unapproved
|
||||||
|
skip_untranslated_strings: true
|
||||||
|
|||||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -42,6 +42,7 @@
|
|||||||
"@mui/joy": "5.0.0-beta.52",
|
"@mui/joy": "5.0.0-beta.52",
|
||||||
"@mui/material": "^5.15.2",
|
"@mui/material": "^5.15.2",
|
||||||
"@openreplay/tracker": "^14.0.4",
|
"@openreplay/tracker": "^14.0.4",
|
||||||
|
"@posthog/rollup-plugin": "^1.4.9",
|
||||||
"@revenuecat/purchases-capacitor": "^12.0.0",
|
"@revenuecat/purchases-capacitor": "^12.0.0",
|
||||||
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
|
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
@@ -4596,6 +4597,49 @@
|
|||||||
"@posthog/types": "^1.402.2"
|
"@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": {
|
"node_modules/@posthog/core": {
|
||||||
"version": "1.47.0",
|
"version": "1.47.0",
|
||||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.47.0.tgz",
|
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.47.0.tgz",
|
||||||
@@ -4605,6 +4649,29 @@
|
|||||||
"@posthog/types": "^1.402.2"
|
"@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": {
|
"node_modules/@posthog/types": {
|
||||||
"version": "1.402.3",
|
"version": "1.402.3",
|
||||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.402.3.tgz",
|
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.402.3.tgz",
|
||||||
@@ -8010,7 +8077,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
"@mui/joy": "5.0.0-beta.52",
|
"@mui/joy": "5.0.0-beta.52",
|
||||||
"@mui/material": "^5.15.2",
|
"@mui/material": "^5.15.2",
|
||||||
"@openreplay/tracker": "^14.0.4",
|
"@openreplay/tracker": "^14.0.4",
|
||||||
|
"@posthog/rollup-plugin": "^1.4.9",
|
||||||
"@revenuecat/purchases-capacitor": "^12.0.0",
|
"@revenuecat/purchases-capacitor": "^12.0.0",
|
||||||
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
|
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"activities": "Activities",
|
"activities": "Activities",
|
||||||
"points": "Points",
|
"points": "Points",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"reportBug": "Report a Bug"
|
"reportBug": "Report an Issue"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Search",
|
"title": "Search",
|
||||||
@@ -41,7 +41,8 @@
|
|||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
"empty": {
|
"empty": {
|
||||||
"title": "No direct matches",
|
"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": {
|
"groups": {
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
@@ -76,7 +77,14 @@
|
|||||||
"open": "Open",
|
"open": "Open",
|
||||||
"results_one": "{{count}} result",
|
"results_one": "{{count}} result",
|
||||||
"results_other": "{{count}} results",
|
"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": {
|
"policyUpdate": {
|
||||||
|
|||||||
@@ -91,8 +91,8 @@
|
|||||||
"description": "Tell us how Donetick is working for you or request a feature."
|
"description": "Tell us how Donetick is working for you or request a feature."
|
||||||
},
|
},
|
||||||
"bugReport": {
|
"bugReport": {
|
||||||
"title": "Report a Bug",
|
"title": "Report an Issue",
|
||||||
"description": "Something not working right? Send us the details along with a technical snapshot."
|
"description": "Tell us what's not working and we'll attach the technical details for you."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Preferences } from '@capacitor/preferences'
|
|||||||
|
|
||||||
// Two independent consent axes, matching the existing onboarding UI
|
// Two independent consent axes, matching the existing onboarding UI
|
||||||
// (HeardAboutView's PrivacyPreferences): "analytics" gates track(), "crash"
|
// (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 = {
|
const CONSENT_KEYS = {
|
||||||
analytics: 'analytics_consent',
|
analytics: 'analytics_consent',
|
||||||
crash: 'analytics_crash_consent',
|
crash: 'analytics_crash_consent',
|
||||||
|
|||||||
@@ -79,12 +79,6 @@ export const EVENT_SCHEMAS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ERROR_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
|
// No message/stack field here by design — those come from the real Error
|
||||||
// object passed to posthog.captureException() itself, not from this
|
// object passed to posthog.captureException() itself, not from this
|
||||||
// sanitized properties bag. This schema only classifies how it was caught.
|
// sanitized properties bag. This schema only classifies how it was caught.
|
||||||
|
|||||||
@@ -142,20 +142,6 @@ export const track = (eventName, properties = {}) => {
|
|||||||
posthog.capture(eventName, sanitized)
|
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
|
* Frontend crashes only. Uses captureException (not capture) so these land
|
||||||
* on PostHog's Error Tracking page with a genuine message + stack trace —
|
* on PostHog's Error Tracking page with a genuine message + stack trace —
|
||||||
@@ -176,7 +162,7 @@ export const captureException = (error, properties = {}) => {
|
|||||||
let globalHandlersInstalled = false
|
let globalHandlersInstalled = false
|
||||||
|
|
||||||
/** Reports uncaught exceptions and unhandled promise rejections to
|
/** 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
|
* Complements, doesn't overlap with, src/views/Error.jsx: that's a React
|
||||||
* Router error-boundary screen for render/loader errors, which React catches
|
* Router error-boundary screen for render/loader errors, which React catches
|
||||||
* before they ever reach window.onerror — a different class of failure, with
|
* 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
|
* kind: 'analytics' | 'crash'. Enabling analytics (re-)initializes PostHog
|
||||||
* if needed and sends analytics_enabled; enabling crash-only never talks to
|
* 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.
|
* Disabling never sends an event and clears identity/queued data.
|
||||||
*/
|
*/
|
||||||
export const setConsent = async (kind, value, { source } = {}) => {
|
export const setConsent = async (kind, value, { source } = {}) => {
|
||||||
@@ -236,7 +222,7 @@ export const setConsent = async (kind, value, { source } = {}) => {
|
|||||||
} else if (kind === 'crash') {
|
} else if (kind === 'crash') {
|
||||||
// Crash reporting alone doesn't need PostHog started with the analytics
|
// Crash reporting alone doesn't need PostHog started with the analytics
|
||||||
// super-properties path, but it does need a live client + identity to
|
// super-properties path, but it does need a live client + identity to
|
||||||
// send captureError() calls through.
|
// send captureException() calls through.
|
||||||
if (isConfigured() && !getClientSync()) {
|
if (isConfigured() && !getClientSync()) {
|
||||||
await startPosthog()
|
await startPosthog()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import { setServerVersion } from '../service/DiagnosticsSession'
|
||||||
import { GetResource } from '../utils/Fetcher'
|
import { GetResource } from '../utils/Fetcher'
|
||||||
|
|
||||||
// Helper to check if we have a valid token
|
// Helper to check if we have a valid token
|
||||||
@@ -18,6 +19,9 @@ export const useResource = () => {
|
|||||||
queryKey: ['resource'],
|
queryKey: ['resource'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await GetResource()
|
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
|
return response
|
||||||
},
|
},
|
||||||
staleTime: 6 * 60 * 60 * 1000, // 6 hours in milliseconds
|
staleTime: 6 * 60 * 60 * 1000, // 6 hours in milliseconds
|
||||||
|
|||||||
@@ -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 = () => {
|
const readRecents = () => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem(RECENTS_KEY)) || []
|
return JSON.parse(localStorage.getItem(RECENTS_KEY)) || []
|
||||||
@@ -268,7 +281,8 @@ const GlobalSearchPalette = ({
|
|||||||
const focusInputRef = useCallback(node => {
|
const focusInputRef = useCallback(node => {
|
||||||
if (node) requestAnimationFrame(() => node.focus())
|
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 [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
const [recents] = useState(readRecents)
|
const [recents] = useState(readRecents)
|
||||||
const selectedResultRef = useRef(null)
|
const selectedResultRef = useRef(null)
|
||||||
@@ -317,8 +331,34 @@ const GlobalSearchPalette = ({
|
|||||||
[documents],
|
[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 results = useMemo(() => {
|
||||||
const normalized = query.trim().toLocaleLowerCase()
|
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) {
|
if (!normalized) {
|
||||||
const currentById = new Map(documents.map(item => [item.id, item]))
|
const currentById = new Map(documents.map(item => [item.id, item]))
|
||||||
const recentResults = recents
|
const recentResults = recents
|
||||||
@@ -347,15 +387,9 @@ const GlobalSearchPalette = ({
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.score - b.score),
|
.sort((a, b) => a.score - b.score),
|
||||||
)
|
)
|
||||||
const actionMatches = (
|
// Keyword-only action matches stay below the real content they share words
|
||||||
quickActionIndex.search(normalized, { limit: 4 }) || []
|
// with; title-prefix matches lead.
|
||||||
)
|
const actionMatches = searchActions(normalized).slice(0, 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.
|
|
||||||
const leadingActions = actionMatches.filter(action =>
|
const leadingActions = actionMatches.filter(action =>
|
||||||
action.title.toLocaleLowerCase().startsWith(normalized),
|
action.title.toLocaleLowerCase().startsWith(normalized),
|
||||||
)
|
)
|
||||||
@@ -375,7 +409,16 @@ const GlobalSearchPalette = ({
|
|||||||
route: `/chores?search=${encodeURIComponent(query.trim())}`,
|
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.
|
// Everything except the always-present "filter the task list" fallback.
|
||||||
const matchCount = results.filter(
|
const matchCount = results.filter(
|
||||||
@@ -395,8 +438,25 @@ const GlobalSearchPalette = ({
|
|||||||
if (presentation === 'modal') onClose()
|
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 => {
|
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()
|
event.preventDefault()
|
||||||
setSelectedIndex(index => Math.min(index + 1, results.length - 1))
|
setSelectedIndex(index => Math.min(index + 1, results.length - 1))
|
||||||
} else if (event.key === 'ArrowUp') {
|
} else if (event.key === 'ArrowUp') {
|
||||||
@@ -423,13 +483,30 @@ const GlobalSearchPalette = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={event => {
|
onChange={event => onQueryChange(event.target.value)}
|
||||||
setQuery(event.target.value)
|
|
||||||
setSelectedIndex(0)
|
|
||||||
}}
|
|
||||||
onKeyDown={onInputKeyDown}
|
onKeyDown={onInputKeyDown}
|
||||||
placeholder={t('search.placeholder')}
|
placeholder={
|
||||||
startDecorator={<SearchRounded />}
|
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={
|
endDecorator={
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<CircularProgress size='sm' />
|
<CircularProgress size='sm' />
|
||||||
@@ -468,7 +545,9 @@ const GlobalSearchPalette = ({
|
|||||||
/>
|
/>
|
||||||
<Typography level='title-md'>{t('search.empty.title')}</Typography>
|
<Typography level='title-md'>{t('search.empty.title')}</Typography>
|
||||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||||
{t('search.empty.subtitle')}
|
{mode
|
||||||
|
? t('search.empty.actionsSubtitle')
|
||||||
|
: t('search.empty.subtitle')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -554,8 +633,13 @@ const GlobalSearchPalette = ({
|
|||||||
↑↓ {t('search.footer.navigate')}
|
↑↓ {t('search.footer.navigate')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography level='body-xs'>↵ {t('search.footer.open')}</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' }}>
|
<Typography level='body-xs' sx={{ ml: 'auto' }}>
|
||||||
{query.trim()
|
{query.trim() || mode
|
||||||
? t('search.footer.results', { count: matchCount })
|
? t('search.footer.results', { count: matchCount })
|
||||||
: t('search.footer.typeToSearch')}
|
: t('search.footer.typeToSearch')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
*
|
*
|
||||||
* Deliberately dependency-free — ApiClient imports it on the request path, so
|
* Deliberately dependency-free — ApiClient imports it on the request path, so
|
||||||
* anything imported here would risk a module cycle. Everything is in memory
|
* 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()
|
const SESSION_STARTED_AT = Date.now()
|
||||||
@@ -25,6 +26,7 @@ const routeTrail = []
|
|||||||
const apiFailures = []
|
const apiFailures = []
|
||||||
let backgroundedCount = 0
|
let backgroundedCount = 0
|
||||||
let serverVersion = null
|
let serverVersion = null
|
||||||
|
let serverCommit = null
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Route trail
|
// Route trail
|
||||||
@@ -67,6 +69,33 @@ export const getPreviousRoute = () =>
|
|||||||
// Server identity
|
// 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
|
* Picks the server build out of response headers. Costs nothing when the
|
||||||
* server doesn't send them — the field simply stays null.
|
* server doesn't send them — the field simply stays null.
|
||||||
@@ -74,21 +103,24 @@ export const getPreviousRoute = () =>
|
|||||||
export const recordServerVersionFromResponse = response => {
|
export const recordServerVersionFromResponse = response => {
|
||||||
if (serverVersion) return
|
if (serverVersion) return
|
||||||
try {
|
try {
|
||||||
serverVersion =
|
rememberServerBuild(
|
||||||
response?.headers?.get?.('x-donetick-version') ||
|
response?.headers?.get?.('x-donetick-version') ||
|
||||||
response?.headers?.get?.('x-api-version') ||
|
response?.headers?.get?.('x-api-version'),
|
||||||
null
|
null,
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
// headers may be inaccessible on opaque responses; not worth reporting
|
// headers may be inaccessible on opaque responses; not worth reporting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setServerVersion = version => {
|
/** Authoritative source: what /resource reports about the backend build. */
|
||||||
if (version) serverVersion = version
|
export const setServerVersion = (version, commit) =>
|
||||||
}
|
rememberServerBuild(version, commit)
|
||||||
|
|
||||||
export const getServerVersion = () => serverVersion
|
export const getServerVersion = () => serverVersion
|
||||||
|
|
||||||
|
export const getServerCommit = () => serverCommit
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// API failures
|
// API failures
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -176,6 +208,7 @@ export const getSessionDiagnostics = async () => {
|
|||||||
navigationType: NAVIGATION_TYPE,
|
navigationType: NAVIGATION_TYPE,
|
||||||
backgroundedCount,
|
backgroundedCount,
|
||||||
serverVersion,
|
serverVersion,
|
||||||
|
serverCommit,
|
||||||
previousRoute: getPreviousRoute(),
|
previousRoute: getPreviousRoute(),
|
||||||
routeTrail: getRouteTrail(),
|
routeTrail: getRouteTrail(),
|
||||||
apiFailures: getApiFailures(),
|
apiFailures: getApiFailures(),
|
||||||
|
|||||||
@@ -153,7 +153,9 @@ export const formatErrorReport = report => {
|
|||||||
session.previousRoute ? `Came from: ${session.previousRoute}` : null,
|
session.previousRoute ? `Came from: ${session.previousRoute}` : null,
|
||||||
'',
|
'',
|
||||||
`App: ${app.appVersion} · ${app.platform}${app.isNative ? ' (native)' : ''}`,
|
`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: ${formatDuration(session.sessionDurationMs)} active · ${
|
||||||
session.navigationType ?? 'unknown'
|
session.navigationType ?? 'unknown'
|
||||||
} start · backgrounded ${session.backgroundedCount ?? 0}×`,
|
} start · backgrounded ${session.backgroundedCount ?? 0}×`,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
|
|
||||||
import { captureError } from '../analytics'
|
|
||||||
import { API_URL } from '../Config'
|
import { API_URL } from '../Config'
|
||||||
import { networkManager } from '../hooks/NetworkManager'
|
import { networkManager } from '../hooks/NetworkManager'
|
||||||
import {
|
import {
|
||||||
normalizeEndpoint,
|
|
||||||
recordApiFailure,
|
recordApiFailure,
|
||||||
recordServerVersionFromResponse,
|
recordServerVersionFromResponse,
|
||||||
} from '../service/DiagnosticsSession'
|
} from '../service/DiagnosticsSession'
|
||||||
@@ -230,11 +228,6 @@ class ApiClient {
|
|||||||
method: config.method,
|
method: config.method,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
})
|
})
|
||||||
captureError('api_error', {
|
|
||||||
http_status: String(response.status),
|
|
||||||
method: config.method || 'GET',
|
|
||||||
operation: normalizeEndpoint(endpoint),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check for 401 (Unauthorized)
|
// 2. Check for 401 (Unauthorized)
|
||||||
@@ -319,11 +312,6 @@ class ApiClient {
|
|||||||
if (!externalAbort) {
|
if (!externalAbort) {
|
||||||
networkManager.setServerUnreachable()
|
networkManager.setServerUnreachable()
|
||||||
recordApiFailure({ endpoint, method: config.method, status: 'network' })
|
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)
|
console.error('Request failed', error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
SearchOff,
|
SearchOff,
|
||||||
SelectAll,
|
SelectAll,
|
||||||
Unarchive,
|
Unarchive,
|
||||||
ViewAgenda,
|
|
||||||
ViewModule,
|
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -48,9 +46,7 @@ import { isOfflineFeatureEnabled } from '../../utils/OfflineFeatureToggle'
|
|||||||
import Priorities from '../../utils/Priorities'
|
import Priorities from '../../utils/Priorities'
|
||||||
import LoadingComponent from '../components/Loading'
|
import LoadingComponent from '../components/Loading'
|
||||||
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
|
||||||
import ChoreCard from './ChoreCard'
|
|
||||||
import ChoreListView from './ChoreListView.jsx'
|
import ChoreListView from './ChoreListView.jsx'
|
||||||
import CompactChoreCard from './CompactChoreCard'
|
|
||||||
import MultiSelectHelp from './MultiSelectHelp'
|
import MultiSelectHelp from './MultiSelectHelp'
|
||||||
|
|
||||||
const sortByUpdatedAtDesc = chores =>
|
const sortByUpdatedAtDesc = chores =>
|
||||||
@@ -108,9 +104,6 @@ const ArchivedTasks = () => {
|
|||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [performers, setPerformers] = useState([])
|
const [performers, setPerformers] = useState([])
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [viewMode, setViewMode] = useState(
|
|
||||||
localStorage.getItem('archivedChoreCardViewMode') || 'default',
|
|
||||||
)
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||||
const searchInputRef = useRef(null)
|
const searchInputRef = useRef(null)
|
||||||
@@ -373,15 +366,6 @@ const ArchivedTasks = () => {
|
|||||||
}
|
}
|
||||||
}, [isMultiSelectMode, selectedChores.size])
|
}, [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 = {
|
const searchOptions = {
|
||||||
keys: ['name', 'raw_label'],
|
keys: ['name', 'raw_label'],
|
||||||
includeScore: true,
|
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) {
|
if (isUserProfileLoading || performers.length === 0 || isLoading) {
|
||||||
return <LoadingComponent />
|
return <LoadingComponent />
|
||||||
}
|
}
|
||||||
@@ -822,26 +786,6 @@ const ArchivedTasks = () => {
|
|||||||
isActive={sortBy !== 'archivedAt' || sortDirection !== 'desc'}
|
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 */}
|
{/* Multi-select Toggle Button */}
|
||||||
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -1137,12 +1081,12 @@ const ArchivedTasks = () => {
|
|||||||
{searchTerm && t('archived.matching', { term: searchTerm })}
|
{searchTerm && t('archived.matching', { term: searchTerm })}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<List sx={{ gap: viewMode === 'compact' ? 0 : 1 }}>
|
<List sx={{ gap: 0 }}>
|
||||||
<ChoreListView
|
<ChoreListView
|
||||||
chores={finalChores}
|
chores={finalChores}
|
||||||
// viewOnly={true}
|
// viewOnly={true}
|
||||||
showActions={false}
|
showActions={false}
|
||||||
viewMode={viewMode}
|
viewMode='compact'
|
||||||
membersData={membersData}
|
membersData={membersData}
|
||||||
isMultiSelectMode={isMultiSelectMode}
|
isMultiSelectMode={isMultiSelectMode}
|
||||||
selectedChores={selectedChores}
|
selectedChores={selectedChores}
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ const CompactChoreCard = ({
|
|||||||
|
|
||||||
return parts.join(' • ')
|
return parts.join(' • ')
|
||||||
}
|
}
|
||||||
|
const showLeadingSlot = showActions || isMultiSelectMode
|
||||||
|
const showTrailingSlot = showActions && !isMultiSelectMode
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -145,7 +147,7 @@ const CompactChoreCard = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Priority bar clickable area */}
|
{/* Priority bar clickable area */}
|
||||||
{chore.priority > 0 && (
|
{chore.priority > 0 && onChipClick && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -170,10 +172,12 @@ const CompactChoreCard = ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 40,
|
width: showLeadingSlot ? 40 : 0,
|
||||||
height: 40,
|
height: 40,
|
||||||
mr: 1.5,
|
mr: showLeadingSlot ? 1.5 : 0,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'width 0.3s ease-in-out, margin 0.3s ease-in-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Complete Button */}
|
{/* Complete Button */}
|
||||||
@@ -434,17 +438,18 @@ const CompactChoreCard = ({
|
|||||||
role='none'
|
role='none'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
|
if (!onChipClick) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onChipClick({ label: l })
|
onChipClick({ label: l })
|
||||||
}}
|
}}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (onChipClick && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onChipClick({ label: l })
|
onChipClick({ label: l })
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: onChipClick ? 'pointer' : 'inherit',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -477,14 +482,14 @@ const CompactChoreCard = ({
|
|||||||
sx={{
|
sx={{
|
||||||
transition:
|
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 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,
|
opacity: showTrailingSlot ? 1 : 0,
|
||||||
transform: isMultiSelectMode
|
transform: showTrailingSlot
|
||||||
? 'translateX(20px) scale(0.8)'
|
? 'translateX(0) scale(1)'
|
||||||
: 'translateX(0) scale(1)',
|
: 'translateX(20px) scale(0.8)',
|
||||||
width: isMultiSelectMode ? 0 : 32,
|
width: showTrailingSlot ? 32 : 0,
|
||||||
marginRight: isMultiSelectMode ? 0 : undefined,
|
marginRight: showTrailingSlot ? undefined : 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
pointerEvents: isMultiSelectMode ? 'none' : 'auto',
|
pointerEvents: showTrailingSlot ? 'auto' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showActions && (
|
{showActions && (
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Browser } from '@capacitor/browser'
|
import { Browser } from '@capacitor/browser'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import {
|
import {
|
||||||
BugReportRounded,
|
|
||||||
CheckRounded,
|
CheckRounded,
|
||||||
ContentCopyRounded,
|
ContentCopyRounded,
|
||||||
ExpandMoreRounded,
|
ExpandMoreRounded,
|
||||||
GitHub,
|
GitHub,
|
||||||
|
ReportProblemRounded,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -165,7 +165,7 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
|
|||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ ...enter(0) }}>
|
<Box sx={{ ...enter(0) }}>
|
||||||
<IconHalo
|
<IconHalo
|
||||||
icon={<BugReportRounded />}
|
icon={<ReportProblemRounded />}
|
||||||
color={isBugReport ? 'warning' : 'danger'}
|
color={isBugReport ? 'warning' : 'danger'}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -175,7 +175,7 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
|
|||||||
level='h4'
|
level='h4'
|
||||||
sx={{ fontWeight: 700, letterSpacing: '-0.01em' }}
|
sx={{ fontWeight: 700, letterSpacing: '-0.01em' }}
|
||||||
>
|
>
|
||||||
{isBugReport ? 'Report a bug' : 'Report this problem'}
|
{isBugReport ? 'Report an issue' : 'Report this problem'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
level='body-sm'
|
level='body-sm'
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { BugReport, ChevronRight, Feedback, Star } from '@mui/icons-material'
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
Feedback,
|
||||||
|
ReportProblem,
|
||||||
|
Star,
|
||||||
|
} from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -52,7 +57,7 @@ const SettingsOverview = () => {
|
|||||||
id: 'bugreport',
|
id: 'bugreport',
|
||||||
title: t('overview.sections.bugReport.title'),
|
title: t('overview.sections.bugReport.title'),
|
||||||
description: t('overview.sections.bugReport.description'),
|
description: t('overview.sections.bugReport.description'),
|
||||||
icon: <BugReport />,
|
icon: <ReportProblem />,
|
||||||
onSelect: () => setBugReportOpen(true),
|
onSelect: () => setBugReportOpen(true),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Capacitor } from '@capacitor/core'
|
|||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
ArrowBack,
|
ArrowBack,
|
||||||
BugReport,
|
|
||||||
FilterAlt,
|
FilterAlt,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
History,
|
History,
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
ListAlt,
|
ListAlt,
|
||||||
Logout,
|
Logout,
|
||||||
MenuRounded,
|
MenuRounded,
|
||||||
|
ReportProblem,
|
||||||
SearchRounded,
|
SearchRounded,
|
||||||
SettingsOutlined,
|
SettingsOutlined,
|
||||||
Toll,
|
Toll,
|
||||||
@@ -305,7 +305,7 @@ const NavBar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemDecorator>
|
<ListItemDecorator>
|
||||||
<BugReport />
|
<ReportProblem />
|
||||||
</ListItemDecorator>
|
</ListItemDecorator>
|
||||||
<ListItemContent>{t('navigation.reportBug')}</ListItemContent>
|
<ListItemContent>{t('navigation.reportBug')}</ListItemContent>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
|||||||
197
vite.config.mjs
197
vite.config.mjs
@@ -1,93 +1,118 @@
|
|||||||
|
import posthog from '@posthog/rollup-plugin'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
import pkg from './package.json'
|
import pkg from './package.json'
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ command }) => ({
|
export default ({ command, mode }) => {
|
||||||
define: {
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
|
const personalApiKey = process.env.POSTHOG_API_KEY || env.POSTHOG_API_KEY
|
||||||
},
|
const projectId = process.env.POSTHOG_PROJECT_ID || env.POSTHOG_PROJECT_ID
|
||||||
// Strip console.* / debugger from production bundles only, so dev logging
|
const host = process.env.POSTHOG_HOST || env.POSTHOG_HOST
|
||||||
// is untouched. `command` is 'build' for `vite build`, 'serve' for the dev server.
|
const uploadPosthogSourcemaps =
|
||||||
esbuild: {
|
command === 'build' && Boolean(personalApiKey && projectId)
|
||||||
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
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
resolve: {
|
return defineConfig({
|
||||||
alias: [
|
define: {
|
||||||
{
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(pkg.version),
|
||||||
find: '@',
|
},
|
||||||
replacement: '/src',
|
// 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user