@ossy/timesheets 3.9.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/TimesheetCard.jsx +2 -0
- package/src/TimesheetPanel.jsx +27 -5
- package/src/TimesheetsFreeTool.jsx +10 -6
- package/src/TimesheetsProductHome.jsx +16 -9
- package/src/auto-generate-timesheets.js +70 -0
- package/src/auto-generate-timesheets.spec.js +90 -0
- package/src/auto-generate-timesheets.task.js +60 -0
- package/src/billing-period.js +123 -0
- package/src/billing-period.spec.js +89 -0
- package/src/build-prefilled-lines.js +3 -16
- package/src/create-and-save-timesheet.flow.js +34 -0
- package/src/create-timesheet-draft.js +81 -0
- package/src/delete-timesheet.flow.js +41 -0
- package/src/disable-timesheets.flow.js +58 -0
- package/src/enable-timesheets.flow.js +24 -0
- package/src/export-csv.action.js +7 -0
- package/src/export-pdf.action.js +7 -0
- package/src/export-png.action.js +7 -0
- package/src/export-timesheet-csv.flow.js +39 -0
- package/src/export-timesheet-pdf.flow.js +39 -0
- package/src/export-timesheet-png.flow.js +39 -0
- package/src/generate.task.js +8 -45
- package/src/index.js +14 -0
- package/src/open-delete.action.js +7 -0
- package/src/open-export.action.js +7 -0
- package/src/send-timesheet-reminder.task.js +76 -0
- package/src/timesheet-dto.js +26 -0
- package/src/timesheet-refs.js +16 -0
- package/src/timesheet-reminder.email.jsx +47 -0
- package/src/timesheet-reminder.js +62 -0
- package/src/timesheet-reminder.spec.js +75 -0
- package/src/timesheet-resources.js +6 -40
- package/src/timesheet-resources.spec.js +1 -1
- package/src/timesheet.schema.js +5 -0
- package/src/timesheets.page.jsx +6 -3
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { viewResource } from '@ossy/resources/server'
|
|
2
|
+
import { TimesheetsSchema } from './schema-ids.js'
|
|
3
|
+
import TimesheetReminderEmail, { subject as assignedSubject } from './timesheet-reminder.email.jsx'
|
|
4
|
+
import {
|
|
5
|
+
formatPeriodLabel,
|
|
6
|
+
resolveTimesheetReminderRecipient,
|
|
7
|
+
shouldNotifyTimesheetCreated,
|
|
8
|
+
timesheetAssignedUrl,
|
|
9
|
+
} from './timesheet-reminder.js'
|
|
10
|
+
import { optionalReference } from './timesheet-refs.js'
|
|
11
|
+
|
|
12
|
+
export const metadata = {
|
|
13
|
+
id: '@ossy/timesheets/tasks/send-reminder',
|
|
14
|
+
triggers: [
|
|
15
|
+
{ type: TimesheetsSchema.timesheet, event: 'Created' },
|
|
16
|
+
],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Email the linked employee when an auto-generated timesheet is created.
|
|
21
|
+
* Manual generate does not notify. Created fires once, so there is no daily cron.
|
|
22
|
+
*/
|
|
23
|
+
export async function run ({ event, log, integrations }) {
|
|
24
|
+
if (!shouldNotifyTimesheetCreated(event)) {
|
|
25
|
+
return { sent: 0, skipped: 1 }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const content = event.payload?.content ?? {}
|
|
29
|
+
const resourceId = event.resourceId
|
|
30
|
+
const timesheet = { id: resourceId, belongsTo: event.payload?.belongsTo, content }
|
|
31
|
+
|
|
32
|
+
const employeeId = optionalReference(content.employeeId)?.resourceId
|
|
33
|
+
const contractId = optionalReference(content.contractId)?.resourceId
|
|
34
|
+
|
|
35
|
+
const [employee, contract] = await Promise.all([
|
|
36
|
+
employeeId ? viewResource(employeeId).catch(() => null) : null,
|
|
37
|
+
contractId ? viewResource(contractId).catch(() => null) : null,
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const recipient = resolveTimesheetReminderRecipient(
|
|
41
|
+
employee ? [employee] : [],
|
|
42
|
+
timesheet,
|
|
43
|
+
)
|
|
44
|
+
if (!recipient) {
|
|
45
|
+
log?.warn(`[timesheets/tasks/send-reminder] no employee email for timesheet ${resourceId}`)
|
|
46
|
+
return { sent: 0, skipped: 1 }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const emailClient = integrations?.get?.('email')
|
|
50
|
+
if (!emailClient) {
|
|
51
|
+
log?.warn('[timesheets/tasks/send-reminder] No email integration available, skipping')
|
|
52
|
+
return { sent: 0, skipped: 1 }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const clientName = contract?.content?.clientName ?? null
|
|
56
|
+
const periodLabel = formatPeriodLabel(content.periodStart, content.periodEnd)
|
|
57
|
+
const timesheetsUrl = timesheetAssignedUrl(resourceId)
|
|
58
|
+
|
|
59
|
+
await emailClient.sendTemplate(
|
|
60
|
+
TimesheetReminderEmail,
|
|
61
|
+
{
|
|
62
|
+
employeeName: recipient.name,
|
|
63
|
+
clientName,
|
|
64
|
+
periodLabel,
|
|
65
|
+
timesheetsUrl,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
to: recipient.email,
|
|
69
|
+
from: 'noreply@ossy.se',
|
|
70
|
+
subject: assignedSubject,
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
log?.info(`[timesheets/tasks/send-reminder] Assigned-timesheet email sent to ${recipient.email}`)
|
|
75
|
+
return { sent: 1, skipped: 0 }
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { optionalReference } from './timesheet-refs.js'
|
|
2
|
+
|
|
3
|
+
export { optionalReference } from './timesheet-refs.js'
|
|
4
|
+
|
|
5
|
+
/** Newest sheet for a period. Generate always creates a new resource; use get-by-id for a specific sheet. */
|
|
6
|
+
export function findTimesheetForPeriod (resources, periodStart) {
|
|
7
|
+
const matches = (resources || []).filter((r) => r.content?.periodStart === periodStart)
|
|
8
|
+
if (matches.length === 0) return null
|
|
9
|
+
return [...matches].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))[0]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function toTimesheetDto (resource) {
|
|
13
|
+
if (!resource) return null
|
|
14
|
+
return {
|
|
15
|
+
id: resource.id,
|
|
16
|
+
periodStart: resource.content?.periodStart,
|
|
17
|
+
periodEnd: resource.content?.periodEnd,
|
|
18
|
+
status: resource.content?.status ?? 'draft',
|
|
19
|
+
holidayLocale: resource.content?.holidayLocale ?? null,
|
|
20
|
+
employeeId: optionalReference(resource.content?.employeeId),
|
|
21
|
+
contractId: optionalReference(resource.content?.contractId),
|
|
22
|
+
lines: resource.content?.lines ?? [],
|
|
23
|
+
createdAt: resource.createdAt,
|
|
24
|
+
updatedAt: resource.updatedAt,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize an optional document reference to `{ resourceId }` or `null`.
|
|
3
|
+
* Accepts a stored object, a plain id string, or empty.
|
|
4
|
+
*/
|
|
5
|
+
export function optionalReference (value) {
|
|
6
|
+
if (value == null || value === '') return null
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
const id = value.trim()
|
|
9
|
+
return id ? { resourceId: id } : null
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'object' && typeof value.resourceId === 'string') {
|
|
12
|
+
const id = value.resourceId.trim()
|
|
13
|
+
return id ? { resourceId: id } : null
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { EmailLayout, EmailButton, EmailText } from '@ossy/email'
|
|
3
|
+
|
|
4
|
+
export const id = '@ossy/timesheets/emails/reminder'
|
|
5
|
+
export const subject = 'En tidrapport har skapats åt dig'
|
|
6
|
+
|
|
7
|
+
export default function TimesheetReminderEmail ({
|
|
8
|
+
employeeName,
|
|
9
|
+
clientName,
|
|
10
|
+
periodLabel,
|
|
11
|
+
timesheetsUrl,
|
|
12
|
+
}) {
|
|
13
|
+
const greeting = employeeName ? `Hej ${employeeName},` : 'Hej,'
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<EmailLayout>
|
|
17
|
+
<h1 style={{ color: '#111111', fontSize: 24, fontWeight: 700, margin: '0 0 24px' }}>
|
|
18
|
+
Du har tilldelats en tidrapport
|
|
19
|
+
</h1>
|
|
20
|
+
|
|
21
|
+
<EmailText>{greeting}</EmailText>
|
|
22
|
+
|
|
23
|
+
<EmailText>
|
|
24
|
+
En tidrapport har skapats åt dig
|
|
25
|
+
{clientName ? (
|
|
26
|
+
<>
|
|
27
|
+
{' '}för uppdraget <strong>{clientName}</strong>
|
|
28
|
+
</>
|
|
29
|
+
) : null}
|
|
30
|
+
{periodLabel ? (
|
|
31
|
+
<>
|
|
32
|
+
{' '}för perioden <strong>{periodLabel}</strong>
|
|
33
|
+
</>
|
|
34
|
+
) : null}
|
|
35
|
+
. Kontrollera timmarna och spara när de stämmer.
|
|
36
|
+
</EmailText>
|
|
37
|
+
|
|
38
|
+
{timesheetsUrl ? (
|
|
39
|
+
<EmailButton href={timesheetsUrl}>Öppna tidrapporten</EmailButton>
|
|
40
|
+
) : null}
|
|
41
|
+
|
|
42
|
+
<EmailText style={{ color: '#888888', fontSize: 13 }}>
|
|
43
|
+
Du får det här mejlet för att en tidrapport skapades automatiskt utifrån ditt avtal.
|
|
44
|
+
</EmailText>
|
|
45
|
+
</EmailLayout>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isEmailLike } from '@ossy/schema'
|
|
2
|
+
import { optionalReference } from './timesheet-refs.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a timesheet Created event should notify the assigned employee.
|
|
6
|
+
* Manual generate does not email; auto-generate does, once, on create.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ payload?: { content?: object }, resourceId?: string } | null | undefined} event
|
|
9
|
+
*/
|
|
10
|
+
export function shouldNotifyTimesheetCreated (event) {
|
|
11
|
+
const content = event?.payload?.content ?? {}
|
|
12
|
+
return content.autoGenerated === true && Boolean(event?.resourceId)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve reminder recipient from the linked employee resource.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array<{ id: string, content?: { name?: string, email?: string } }>} employees
|
|
19
|
+
* @param {{ content?: { employeeId?: unknown } }} timesheet
|
|
20
|
+
* @returns {{ email: string, name: string | null } | null}
|
|
21
|
+
*/
|
|
22
|
+
export function resolveTimesheetReminderRecipient (employees, timesheet) {
|
|
23
|
+
const employeeId = optionalReference(timesheet?.content?.employeeId)?.resourceId
|
|
24
|
+
if (!employeeId) return null
|
|
25
|
+
|
|
26
|
+
const employee = (employees || []).find((e) => e.id === employeeId)
|
|
27
|
+
const email = employee?.content?.email?.trim?.() ?? employee?.content?.email
|
|
28
|
+
if (!isEmailLike(email)) return null
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
email: String(email).trim(),
|
|
32
|
+
name: employee?.content?.name ?? null,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Public timesheets home URL, optionally opening a specific sheet (`?r=`).
|
|
38
|
+
* Swedish email copy uses `/tidrapporter`.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} [resourceId]
|
|
41
|
+
* @param {string} [origin]
|
|
42
|
+
*/
|
|
43
|
+
export function timesheetAssignedUrl (resourceId, origin = process.env.WEB_CLIENT_DOMAIN) {
|
|
44
|
+
const base = String(origin || '').replace(/\/$/, '')
|
|
45
|
+
if (!base) return null
|
|
46
|
+
const path = '/tidrapporter'
|
|
47
|
+
if (!resourceId) return `${base}${path}`
|
|
48
|
+
return `${base}${path}?r=${encodeURIComponent(resourceId)}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format a period for email copy (local calendar dates).
|
|
53
|
+
* @param {number} periodStart
|
|
54
|
+
* @param {number} periodEnd
|
|
55
|
+
* @param {string} [locale]
|
|
56
|
+
*/
|
|
57
|
+
export function formatPeriodLabel (periodStart, periodEnd, locale = 'sv-SE') {
|
|
58
|
+
const start = new Date(periodStart)
|
|
59
|
+
const end = new Date(periodEnd)
|
|
60
|
+
const opts = { year: 'numeric', month: 'long', day: 'numeric' }
|
|
61
|
+
return `${start.toLocaleDateString(locale, opts)} – ${end.toLocaleDateString(locale, opts)}`
|
|
62
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals'
|
|
2
|
+
import {
|
|
3
|
+
formatPeriodLabel,
|
|
4
|
+
resolveTimesheetReminderRecipient,
|
|
5
|
+
shouldNotifyTimesheetCreated,
|
|
6
|
+
timesheetAssignedUrl,
|
|
7
|
+
} from './timesheet-reminder.js'
|
|
8
|
+
|
|
9
|
+
describe('shouldNotifyTimesheetCreated', () => {
|
|
10
|
+
it('notifies only auto-generated creates', () => {
|
|
11
|
+
expect(shouldNotifyTimesheetCreated({
|
|
12
|
+
resourceId: 'sheet-1',
|
|
13
|
+
payload: { content: { autoGenerated: true } },
|
|
14
|
+
})).toBe(true)
|
|
15
|
+
|
|
16
|
+
expect(shouldNotifyTimesheetCreated({
|
|
17
|
+
resourceId: 'sheet-2',
|
|
18
|
+
payload: { content: { autoGenerated: false } },
|
|
19
|
+
})).toBe(false)
|
|
20
|
+
|
|
21
|
+
expect(shouldNotifyTimesheetCreated({
|
|
22
|
+
resourceId: 'sheet-3',
|
|
23
|
+
payload: { content: {} },
|
|
24
|
+
})).toBe(false)
|
|
25
|
+
|
|
26
|
+
expect(shouldNotifyTimesheetCreated({
|
|
27
|
+
payload: { content: { autoGenerated: true } },
|
|
28
|
+
})).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('resolveTimesheetReminderRecipient', () => {
|
|
33
|
+
it('requires a linked employee with a valid email', () => {
|
|
34
|
+
const employees = [
|
|
35
|
+
{ id: 'emp-1', content: { name: 'Ada', email: 'ada@example.com' } },
|
|
36
|
+
{ id: 'emp-2', content: { name: 'Bob', email: 'not-an-email' } },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
expect(resolveTimesheetReminderRecipient(employees, {
|
|
40
|
+
content: { employeeId: 'emp-1' },
|
|
41
|
+
})).toEqual({ email: 'ada@example.com', name: 'Ada' })
|
|
42
|
+
|
|
43
|
+
expect(resolveTimesheetReminderRecipient(employees, {
|
|
44
|
+
content: { employeeId: 'emp-2' },
|
|
45
|
+
})).toBe(null)
|
|
46
|
+
|
|
47
|
+
expect(resolveTimesheetReminderRecipient(employees, {
|
|
48
|
+
content: {},
|
|
49
|
+
})).toBe(null)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('timesheetAssignedUrl', () => {
|
|
54
|
+
it('builds the Swedish home URL with an optional sheet query', () => {
|
|
55
|
+
expect(timesheetAssignedUrl(undefined, 'https://app.ossy.se/')).toBe(
|
|
56
|
+
'https://app.ossy.se/tidrapporter',
|
|
57
|
+
)
|
|
58
|
+
expect(timesheetAssignedUrl('sheet-1', 'https://app.ossy.se')).toBe(
|
|
59
|
+
'https://app.ossy.se/tidrapporter?r=sheet-1',
|
|
60
|
+
)
|
|
61
|
+
expect(timesheetAssignedUrl('sheet-1', '')).toBe(null)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('formatPeriodLabel', () => {
|
|
66
|
+
it('formats inclusive local dates', () => {
|
|
67
|
+
const label = formatPeriodLabel(
|
|
68
|
+
new Date(2026, 7, 1).getTime(),
|
|
69
|
+
new Date(2026, 7, 31).getTime(),
|
|
70
|
+
'en-US',
|
|
71
|
+
)
|
|
72
|
+
expect(label).toContain('2026')
|
|
73
|
+
expect(label).toContain('–')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ResourcesQueries } from '@ossy/resources/server'
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
optionalReference,
|
|
5
|
+
findTimesheetForPeriod,
|
|
6
|
+
toTimesheetDto,
|
|
7
|
+
} from './timesheet-dto.js'
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* List timesheet resources by schema id (and optional workspace).
|
|
5
11
|
*/
|
|
@@ -8,43 +14,3 @@ export function getTimesheetResources ({ schemaId, belongsTo } = {}) {
|
|
|
8
14
|
if (belongsTo) query.belongsTo = belongsTo
|
|
9
15
|
return ResourcesQueries.GetResources(query)
|
|
10
16
|
}
|
|
11
|
-
|
|
12
|
-
/** Newest sheet for a period. Generate always creates a new resource; use get-by-id for a specific sheet. */
|
|
13
|
-
export function findTimesheetForPeriod (resources, periodStart) {
|
|
14
|
-
const matches = (resources || []).filter((r) => r.content?.periodStart === periodStart)
|
|
15
|
-
if (matches.length === 0) return null
|
|
16
|
-
return [...matches].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))[0]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Normalize an optional document reference to `{ resourceId }` or `null`.
|
|
21
|
-
* Accepts a stored object, a plain id string, or empty.
|
|
22
|
-
*/
|
|
23
|
-
export function optionalReference (value) {
|
|
24
|
-
if (value == null || value === '') return null
|
|
25
|
-
if (typeof value === 'string') {
|
|
26
|
-
const id = value.trim()
|
|
27
|
-
return id ? { resourceId: id } : null
|
|
28
|
-
}
|
|
29
|
-
if (typeof value === 'object' && typeof value.resourceId === 'string') {
|
|
30
|
-
const id = value.resourceId.trim()
|
|
31
|
-
return id ? { resourceId: id } : null
|
|
32
|
-
}
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function toTimesheetDto (resource) {
|
|
37
|
-
if (!resource) return null
|
|
38
|
-
return {
|
|
39
|
-
id: resource.id,
|
|
40
|
-
periodStart: resource.content?.periodStart,
|
|
41
|
-
periodEnd: resource.content?.periodEnd,
|
|
42
|
-
status: resource.content?.status ?? 'draft',
|
|
43
|
-
holidayLocale: resource.content?.holidayLocale ?? null,
|
|
44
|
-
employeeId: optionalReference(resource.content?.employeeId),
|
|
45
|
-
contractId: optionalReference(resource.content?.contractId),
|
|
46
|
-
lines: resource.content?.lines ?? [],
|
|
47
|
-
createdAt: resource.createdAt,
|
|
48
|
-
updatedAt: resource.updatedAt,
|
|
49
|
-
}
|
|
50
|
-
}
|
package/src/timesheet.schema.js
CHANGED
|
@@ -38,6 +38,11 @@ export default {
|
|
|
38
38
|
of: TimesheetRefs.contract,
|
|
39
39
|
description: 'Optional contract this timesheet belongs to',
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: 'autoGenerated',
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Set when the daily auto-generate cron created this sheet',
|
|
45
|
+
},
|
|
41
46
|
// `lines` is stored in content (array of day rows) — same nested-content pattern as booking availability.
|
|
42
47
|
],
|
|
43
48
|
}
|
package/src/timesheets.page.jsx
CHANGED
|
@@ -20,11 +20,12 @@ export default function TimesheetsHomePage () {
|
|
|
20
20
|
const app = useApp()
|
|
21
21
|
const router = useRouter()
|
|
22
22
|
const { workspace: shellWorkspace } = useShellWorkspace()
|
|
23
|
-
const { read, invoke
|
|
23
|
+
const { read, invoke } = useSdk()
|
|
24
24
|
const isAuthenticated = !!app?.isAuthenticated
|
|
25
25
|
const {
|
|
26
26
|
data: workspaceFromSdk,
|
|
27
27
|
status: workspaceStatus,
|
|
28
|
+
refetch: refetchWorkspace,
|
|
28
29
|
} = read(GetWorkspace, undefined, { enabled: isAuthenticated })
|
|
29
30
|
const autoEnableStarted = useRef(false)
|
|
30
31
|
|
|
@@ -52,10 +53,12 @@ export default function TimesheetsHomePage () {
|
|
|
52
53
|
&& workspaceStatus !== AsyncStatus.Error
|
|
53
54
|
&& app?.workspaceServices == null
|
|
54
55
|
|
|
56
|
+
// Shared GetWorkspace cache with useShellWorkspace — refetch (not bare invalidate)
|
|
57
|
+
// awaits the post-enable read so home can switch sales → product reliably.
|
|
55
58
|
const enableService = useCallback(() => {
|
|
56
59
|
return invoke(EnableService, { service: TIMESHEETS_SERVICE })
|
|
57
|
-
.then(() =>
|
|
58
|
-
}, [invoke,
|
|
60
|
+
.then(() => refetchWorkspace())
|
|
61
|
+
}, [invoke, refetchWorkspace])
|
|
59
62
|
|
|
60
63
|
useEffect(() => {
|
|
61
64
|
if (!isAuthenticated || !enableRequested || autoEnableStarted.current) return
|