@tuturuuu/ui 0.1.0 → 0.3.1
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/CHANGELOG.md +71 -0
- package/package.json +82 -70
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -3
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/legacy/meet/planId/page.tsx +10 -4
- package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
- package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/__tests__/use-task-realtime-sync.test.tsx +37 -9
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +89 -70
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useLocalStorage } from '@tuturuuu/ui/hooks/use-local-storage';
|
|
4
|
-
import {
|
|
5
|
-
useUpdateUserConfig,
|
|
6
|
-
useUserConfig,
|
|
7
|
-
} from '@tuturuuu/ui/hooks/use-user-config';
|
|
8
4
|
import { setCookie } from 'cookies-next';
|
|
9
5
|
import {
|
|
6
|
+
type ComponentType,
|
|
10
7
|
createContext,
|
|
11
8
|
type Dispatch,
|
|
12
9
|
type ReactNode,
|
|
@@ -14,7 +11,6 @@ import {
|
|
|
14
11
|
useCallback,
|
|
15
12
|
useContext,
|
|
16
13
|
useEffect,
|
|
17
|
-
useRef,
|
|
18
14
|
useState,
|
|
19
15
|
} from 'react';
|
|
20
16
|
|
|
@@ -23,9 +19,6 @@ export const SIDEBAR_BEHAVIOR_CONFIG_KEY = 'SIDEBAR_BEHAVIOR';
|
|
|
23
19
|
|
|
24
20
|
export type SidebarBehavior = 'expanded' | 'collapsed' | 'hover';
|
|
25
21
|
|
|
26
|
-
const isValidBehavior = (value: string | undefined): value is SidebarBehavior =>
|
|
27
|
-
value === 'expanded' || value === 'collapsed' || value === 'hover';
|
|
28
|
-
|
|
29
22
|
interface SidebarContextProps {
|
|
30
23
|
behavior: SidebarBehavior;
|
|
31
24
|
setBehavior: Dispatch<SetStateAction<SidebarBehavior>>;
|
|
@@ -42,6 +35,38 @@ export const SidebarContext = createContext<SidebarContextProps | undefined>(
|
|
|
42
35
|
// Persistent cookie options — ensures setting survives browser restarts
|
|
43
36
|
const COOKIE_OPTIONS = { maxAge: 365 * 24 * 60 * 60, path: '/' } as const;
|
|
44
37
|
|
|
38
|
+
type SidebarRemoteBehaviorBridgeComponent = ComponentType<{
|
|
39
|
+
behavior: SidebarBehavior;
|
|
40
|
+
localOverride: boolean;
|
|
41
|
+
localOverrideVersion: number;
|
|
42
|
+
onApplyRemoteBehavior: (newBehavior: SidebarBehavior) => void;
|
|
43
|
+
onRemoteBehaviorAvailable: (remoteBehavior: SidebarBehavior) => void;
|
|
44
|
+
userChangeVersion: number;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
function useSidebarRemoteBehaviorBridge() {
|
|
48
|
+
const [RemoteBehaviorBridge, setRemoteBehaviorBridge] =
|
|
49
|
+
useState<SidebarRemoteBehaviorBridgeComponent | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let active = true;
|
|
53
|
+
|
|
54
|
+
// biome-ignore lint/suspicious/noTsIgnore: NodeNext requires .js, but Next/Turbopack resolves workspace TypeScript source here before package emit.
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
void import('./sidebar-remote-behavior-bridge').then((module) => {
|
|
57
|
+
if (active) {
|
|
58
|
+
setRemoteBehaviorBridge(() => module.SidebarRemoteBehaviorBridge);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
active = false;
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return RemoteBehaviorBridge;
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
export const SidebarProvider = ({
|
|
46
71
|
children,
|
|
47
72
|
initialBehavior,
|
|
@@ -54,74 +79,39 @@ export const SidebarProvider = ({
|
|
|
54
79
|
'sidebar-local-override',
|
|
55
80
|
false
|
|
56
81
|
);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const userChangedRef = useRef(false);
|
|
60
|
-
// Allows reading current behavior inside the sync effect without
|
|
61
|
-
// adding `behavior` to its dependency array
|
|
62
|
-
const behaviorRef = useRef(behavior);
|
|
63
|
-
behaviorRef.current = behavior;
|
|
64
|
-
|
|
65
|
-
// Fetch account-wide preference
|
|
66
|
-
const { data: remoteBehavior, isSuccess: remoteLoaded } = useUserConfig(
|
|
67
|
-
SIDEBAR_BEHAVIOR_CONFIG_KEY,
|
|
68
|
-
'expanded'
|
|
82
|
+
const [remoteBehavior, setRemoteBehavior] = useState<SidebarBehavior | null>(
|
|
83
|
+
null
|
|
69
84
|
);
|
|
85
|
+
const [userChangeVersion, setUserChangeVersion] = useState(0);
|
|
86
|
+
const [localOverrideVersion, setLocalOverrideVersion] = useState(0);
|
|
87
|
+
const RemoteBehaviorBridge = useSidebarRemoteBehaviorBridge();
|
|
70
88
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
!remoteLoaded ||
|
|
77
|
-
localOverride ||
|
|
78
|
-
hasAppliedRemote.current ||
|
|
79
|
-
userChangedRef.current
|
|
80
|
-
)
|
|
81
|
-
return;
|
|
82
|
-
hasAppliedRemote.current = true;
|
|
83
|
-
if (
|
|
84
|
-
isValidBehavior(remoteBehavior) &&
|
|
85
|
-
remoteBehavior !== behaviorRef.current
|
|
86
|
-
) {
|
|
87
|
-
setBehavior(remoteBehavior);
|
|
88
|
-
setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, remoteBehavior, COOKIE_OPTIONS);
|
|
89
|
-
}
|
|
90
|
-
}, [remoteLoaded, remoteBehavior, localOverride]);
|
|
89
|
+
const applyBehavior = useCallback((newBehavior: SidebarBehavior) => {
|
|
90
|
+
setBehavior(newBehavior);
|
|
91
|
+
setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, newBehavior, COOKIE_OPTIONS);
|
|
92
|
+
}, []);
|
|
91
93
|
|
|
92
94
|
const handleBehaviorChange = useCallback(
|
|
93
95
|
(newBehavior: SidebarBehavior) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Always update cookie for SSR
|
|
97
|
-
setCookie(SIDEBAR_BEHAVIOR_COOKIE_NAME, newBehavior, COOKIE_OPTIONS);
|
|
98
|
-
// Save to user_configs (account-wide) unless locally overridden
|
|
96
|
+
applyBehavior(newBehavior);
|
|
97
|
+
|
|
99
98
|
if (!localOverride) {
|
|
100
|
-
|
|
101
|
-
configId: SIDEBAR_BEHAVIOR_CONFIG_KEY,
|
|
102
|
-
value: newBehavior,
|
|
103
|
-
});
|
|
99
|
+
setUserChangeVersion((version) => version + 1);
|
|
104
100
|
}
|
|
105
101
|
},
|
|
106
|
-
[
|
|
102
|
+
[applyBehavior, localOverride]
|
|
107
103
|
);
|
|
108
104
|
|
|
109
105
|
const setLocalOverride = useCallback(
|
|
110
106
|
(enabled: boolean) => {
|
|
111
107
|
setLocalOverrideRaw(enabled);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else if (enabled) {
|
|
117
|
-
// Turning on local override — save current behavior to account-wide first
|
|
118
|
-
updateConfig.mutate({
|
|
119
|
-
configId: SIDEBAR_BEHAVIOR_CONFIG_KEY,
|
|
120
|
-
value: behavior,
|
|
121
|
-
});
|
|
108
|
+
setLocalOverrideVersion((version) => version + 1);
|
|
109
|
+
|
|
110
|
+
if (!enabled && remoteBehavior) {
|
|
111
|
+
applyBehavior(remoteBehavior);
|
|
122
112
|
}
|
|
123
113
|
},
|
|
124
|
-
[
|
|
114
|
+
[applyBehavior, remoteBehavior, setLocalOverrideRaw]
|
|
125
115
|
);
|
|
126
116
|
|
|
127
117
|
return (
|
|
@@ -134,6 +124,16 @@ export const SidebarProvider = ({
|
|
|
134
124
|
setLocalOverride,
|
|
135
125
|
}}
|
|
136
126
|
>
|
|
127
|
+
{RemoteBehaviorBridge && (
|
|
128
|
+
<RemoteBehaviorBridge
|
|
129
|
+
behavior={behavior}
|
|
130
|
+
localOverride={localOverride}
|
|
131
|
+
localOverrideVersion={localOverrideVersion}
|
|
132
|
+
onApplyRemoteBehavior={applyBehavior}
|
|
133
|
+
onRemoteBehaviorAvailable={setRemoteBehavior}
|
|
134
|
+
userChangeVersion={userChangeVersion}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
137
|
{children}
|
|
138
138
|
</SidebarContext.Provider>
|
|
139
139
|
);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useUpdateUserConfig,
|
|
5
|
+
useUserConfig,
|
|
6
|
+
} from '@tuturuuu/ui/hooks/use-user-config';
|
|
7
|
+
import { useEffect, useRef } from 'react';
|
|
8
|
+
|
|
9
|
+
type SidebarBehavior = 'expanded' | 'collapsed' | 'hover';
|
|
10
|
+
|
|
11
|
+
const SIDEBAR_BEHAVIOR_CONFIG_KEY = 'SIDEBAR_BEHAVIOR';
|
|
12
|
+
|
|
13
|
+
const isValidBehavior = (value: string | undefined): value is SidebarBehavior =>
|
|
14
|
+
value === 'expanded' || value === 'collapsed' || value === 'hover';
|
|
15
|
+
|
|
16
|
+
interface SidebarRemoteBehaviorBridgeProps {
|
|
17
|
+
behavior: SidebarBehavior;
|
|
18
|
+
localOverride: boolean;
|
|
19
|
+
localOverrideVersion: number;
|
|
20
|
+
onApplyRemoteBehavior: (newBehavior: SidebarBehavior) => void;
|
|
21
|
+
onRemoteBehaviorAvailable: (remoteBehavior: SidebarBehavior) => void;
|
|
22
|
+
userChangeVersion: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function SidebarRemoteBehaviorBridge({
|
|
26
|
+
behavior,
|
|
27
|
+
localOverride,
|
|
28
|
+
localOverrideVersion,
|
|
29
|
+
onApplyRemoteBehavior,
|
|
30
|
+
onRemoteBehaviorAvailable,
|
|
31
|
+
userChangeVersion,
|
|
32
|
+
}: SidebarRemoteBehaviorBridgeProps) {
|
|
33
|
+
const { data: remoteBehavior, isSuccess: remoteLoaded } = useUserConfig(
|
|
34
|
+
SIDEBAR_BEHAVIOR_CONFIG_KEY,
|
|
35
|
+
'expanded'
|
|
36
|
+
);
|
|
37
|
+
const updateConfig = useUpdateUserConfig();
|
|
38
|
+
const hasAppliedRemote = useRef(false);
|
|
39
|
+
const persistedUserChangeVersion = useRef(0);
|
|
40
|
+
const handledLocalOverrideVersion = useRef(0);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!remoteLoaded || !isValidBehavior(remoteBehavior)) return;
|
|
44
|
+
|
|
45
|
+
onRemoteBehaviorAvailable(remoteBehavior);
|
|
46
|
+
}, [onRemoteBehaviorAvailable, remoteBehavior, remoteLoaded]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (
|
|
50
|
+
!remoteLoaded ||
|
|
51
|
+
localOverride ||
|
|
52
|
+
hasAppliedRemote.current ||
|
|
53
|
+
userChangeVersion > 0 ||
|
|
54
|
+
!isValidBehavior(remoteBehavior)
|
|
55
|
+
) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
hasAppliedRemote.current = true;
|
|
60
|
+
|
|
61
|
+
if (remoteBehavior !== behavior) {
|
|
62
|
+
onApplyRemoteBehavior(remoteBehavior);
|
|
63
|
+
}
|
|
64
|
+
}, [
|
|
65
|
+
behavior,
|
|
66
|
+
localOverride,
|
|
67
|
+
onApplyRemoteBehavior,
|
|
68
|
+
remoteBehavior,
|
|
69
|
+
remoteLoaded,
|
|
70
|
+
userChangeVersion,
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (
|
|
75
|
+
!remoteLoaded ||
|
|
76
|
+
localOverride ||
|
|
77
|
+
userChangeVersion === 0 ||
|
|
78
|
+
persistedUserChangeVersion.current === userChangeVersion
|
|
79
|
+
) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
persistedUserChangeVersion.current = userChangeVersion;
|
|
84
|
+
updateConfig.mutate({
|
|
85
|
+
configId: SIDEBAR_BEHAVIOR_CONFIG_KEY,
|
|
86
|
+
value: behavior,
|
|
87
|
+
});
|
|
88
|
+
}, [behavior, localOverride, remoteLoaded, updateConfig, userChangeVersion]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (
|
|
92
|
+
!remoteLoaded ||
|
|
93
|
+
localOverrideVersion === 0 ||
|
|
94
|
+
handledLocalOverrideVersion.current === localOverrideVersion
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
handledLocalOverrideVersion.current = localOverrideVersion;
|
|
100
|
+
|
|
101
|
+
if (localOverride) {
|
|
102
|
+
updateConfig.mutate({
|
|
103
|
+
configId: SIDEBAR_BEHAVIOR_CONFIG_KEY,
|
|
104
|
+
value: behavior,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isValidBehavior(remoteBehavior)) {
|
|
110
|
+
onApplyRemoteBehavior(remoteBehavior);
|
|
111
|
+
}
|
|
112
|
+
}, [
|
|
113
|
+
behavior,
|
|
114
|
+
localOverride,
|
|
115
|
+
localOverrideVersion,
|
|
116
|
+
onApplyRemoteBehavior,
|
|
117
|
+
remoteBehavior,
|
|
118
|
+
remoteLoaded,
|
|
119
|
+
updateConfig,
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Absolute URL to the Tuturuuu logo hosted on the production domain. */
|
|
2
|
+
export const TUTURUUU_REMOTE_LOGO_URL =
|
|
3
|
+
'https://tuturuuu.com/media/logos/transparent.png';
|
|
4
|
+
|
|
5
|
+
export const TUTURUUU_LOCAL_LOGO_URL = '/media/logos/transparent.png';
|
|
6
|
+
export const TUTURUUU_LOGO_URL = TUTURUUU_REMOTE_LOGO_URL;
|
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
import Image from 'next/image';
|
|
1
|
+
import Image, { type ImageProps } from 'next/image';
|
|
2
|
+
import { TUTURUUU_LOGO_URL } from './tuturuuu-logo-urls';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export {
|
|
5
|
+
TUTURUUU_LOCAL_LOGO_URL,
|
|
6
|
+
TUTURUUU_LOGO_URL,
|
|
7
|
+
TUTURUUU_REMOTE_LOGO_URL,
|
|
8
|
+
} from './tuturuuu-logo-urls';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Convenience wrapper around `<Image>` pre-configured with the Tuturuuu logo.
|
|
9
|
-
*
|
|
12
|
+
* Remote logo usage stays unoptimized so no per-app `remotePatterns` config is required.
|
|
10
13
|
*/
|
|
11
14
|
export function TuturuuLogo({
|
|
12
15
|
alt = 'Tuturuuu Logo',
|
|
16
|
+
src = TUTURUUU_LOGO_URL,
|
|
17
|
+
unoptimized,
|
|
13
18
|
...props
|
|
14
|
-
}: Omit<
|
|
15
|
-
|
|
19
|
+
}: Omit<ImageProps, 'alt' | 'src'> & {
|
|
20
|
+
alt?: string;
|
|
21
|
+
src?: ImageProps['src'];
|
|
22
|
+
}) {
|
|
23
|
+
const shouldSkipOptimization =
|
|
24
|
+
unoptimized ?? (typeof src === 'string' && /^https?:\/\//u.test(src));
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Image
|
|
28
|
+
src={src}
|
|
29
|
+
alt={alt}
|
|
30
|
+
unoptimized={shouldSkipOptimization}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
16
34
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InternalApiWorkspaceSummary } from '@tuturuuu/types';
|
|
2
|
+
|
|
3
|
+
export function mergeWorkspaceSelectWorkspaces(
|
|
4
|
+
workspaces: InternalApiWorkspaceSummary[] | undefined,
|
|
5
|
+
currentWorkspaceFallback: InternalApiWorkspaceSummary | null | undefined
|
|
6
|
+
) {
|
|
7
|
+
const workspaceList = workspaces ?? [];
|
|
8
|
+
|
|
9
|
+
if (!currentWorkspaceFallback) return workspaceList;
|
|
10
|
+
|
|
11
|
+
if (
|
|
12
|
+
workspaceList.some(
|
|
13
|
+
(workspace) => workspace.id === currentWorkspaceFallback.id
|
|
14
|
+
)
|
|
15
|
+
) {
|
|
16
|
+
return workspaceList;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return [...workspaceList, currentWorkspaceFallback];
|
|
20
|
+
}
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
Star,
|
|
11
11
|
} from '@tuturuuu/icons';
|
|
12
12
|
import { updateCurrentUserDefaultWorkspace } from '@tuturuuu/internal-api/users';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
acceptWorkspaceInvite,
|
|
15
|
+
getWorkspace,
|
|
16
|
+
} from '@tuturuuu/internal-api/workspaces';
|
|
14
17
|
import type { InternalApiWorkspaceSummary } from '@tuturuuu/types';
|
|
15
18
|
import type { WorkspaceUser } from '@tuturuuu/types/primitives/WorkspaceUser';
|
|
16
19
|
import {
|
|
@@ -63,6 +66,7 @@ import {
|
|
|
63
66
|
import { Input } from '../input';
|
|
64
67
|
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
65
68
|
import { TUTURUUU_LOGO_URL } from './tuturuuu-logo';
|
|
69
|
+
import { mergeWorkspaceSelectWorkspaces } from './workspace-select-helpers';
|
|
66
70
|
|
|
67
71
|
const FormSchema = z.object({
|
|
68
72
|
name: z.string().min(1).max(100),
|
|
@@ -161,17 +165,34 @@ export function WorkspaceSelect({
|
|
|
161
165
|
const pathname = usePathname();
|
|
162
166
|
const queryClient = useQueryClient();
|
|
163
167
|
|
|
164
|
-
const
|
|
168
|
+
const resolvedWorkspaceId =
|
|
169
|
+
wsId && wsId !== PERSONAL_WORKSPACE_SLUG
|
|
170
|
+
? resolveWorkspaceId(wsId)
|
|
171
|
+
: undefined;
|
|
172
|
+
const { data: listedWorkspaces } = useQuery({
|
|
165
173
|
queryKey: ['workspaces'],
|
|
166
174
|
queryFn: fetchWorkspaces,
|
|
167
175
|
enabled: !!wsId,
|
|
168
176
|
});
|
|
177
|
+
const hasListedCurrentWorkspace = Boolean(
|
|
178
|
+
resolvedWorkspaceId &&
|
|
179
|
+
listedWorkspaces?.some(
|
|
180
|
+
(workspace) => workspace.id === resolvedWorkspaceId
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
const { data: currentWorkspaceFallback } = useQuery({
|
|
184
|
+
queryKey: ['workspace-select-current-workspace', resolvedWorkspaceId],
|
|
185
|
+
queryFn: async () =>
|
|
186
|
+
(await getWorkspace(resolvedWorkspaceId!)) as InternalApiWorkspaceSummary,
|
|
187
|
+
enabled: Boolean(resolvedWorkspaceId && !hasListedCurrentWorkspace),
|
|
188
|
+
retry: 1,
|
|
189
|
+
});
|
|
190
|
+
const workspaces = mergeWorkspaceSelectWorkspaces(
|
|
191
|
+
listedWorkspaces,
|
|
192
|
+
currentWorkspaceFallback
|
|
193
|
+
);
|
|
169
194
|
const { data: currentUser } = useWorkspaceUser();
|
|
170
195
|
|
|
171
|
-
const resolvedWorkspaceId =
|
|
172
|
-
wsId && wsId !== PERSONAL_WORKSPACE_SLUG
|
|
173
|
-
? resolveWorkspaceId(wsId)
|
|
174
|
-
: undefined;
|
|
175
196
|
const defaultWorkspaceId = currentUser?.default_workspace_id || null;
|
|
176
197
|
|
|
177
198
|
const form = useForm({
|
|
@@ -435,9 +456,12 @@ export function WorkspaceSelect({
|
|
|
435
456
|
const workspace =
|
|
436
457
|
wsId === PERSONAL_WORKSPACE_SLUG
|
|
437
458
|
? personalWorkspace
|
|
438
|
-
: workspaces
|
|
459
|
+
: (workspaces.find((ws) => ws.id === resolvedWorkspaceId) ??
|
|
460
|
+
guestWorkspaces.find((ws) => ws.id === resolvedWorkspaceId));
|
|
439
461
|
if (!wsId) return <div />;
|
|
440
462
|
|
|
463
|
+
const hasSelectableWorkspaces = workspaces.length > 0;
|
|
464
|
+
|
|
441
465
|
async function onJoinByHandleSubmit(
|
|
442
466
|
formData: z.infer<typeof JoinWorkspaceByHandleFormSchema>
|
|
443
467
|
) {
|
|
@@ -540,10 +564,7 @@ export function WorkspaceSelect({
|
|
|
540
564
|
}}
|
|
541
565
|
>
|
|
542
566
|
<Popover open={open} onOpenChange={setOpen}>
|
|
543
|
-
<PopoverTrigger
|
|
544
|
-
asChild
|
|
545
|
-
disabled={!workspaces || workspaces.length === 0}
|
|
546
|
-
>
|
|
567
|
+
<PopoverTrigger asChild disabled={!hasSelectableWorkspaces}>
|
|
547
568
|
<Button
|
|
548
569
|
size="xs"
|
|
549
570
|
variant="outline"
|
|
@@ -553,7 +574,7 @@ export function WorkspaceSelect({
|
|
|
553
574
|
hideLeading ? 'justify-center p-0' : 'justify-start',
|
|
554
575
|
'w-full whitespace-normal text-start'
|
|
555
576
|
)}
|
|
556
|
-
disabled={!
|
|
577
|
+
disabled={!hasSelectableWorkspaces}
|
|
557
578
|
>
|
|
558
579
|
<WorkspaceIcon
|
|
559
580
|
fallbackLogoUrl={fallbackLogoUrl}
|
|
@@ -45,7 +45,10 @@ export function InvoiceCheckoutSummary({
|
|
|
45
45
|
<span className="text-muted-foreground">
|
|
46
46
|
{t('ws-invoices.subtotal')}
|
|
47
47
|
</span>
|
|
48
|
-
<FinanceDisplayAmount
|
|
48
|
+
<FinanceDisplayAmount
|
|
49
|
+
alwaysShow
|
|
50
|
+
value={formatCurrency(subtotal, currency)}
|
|
51
|
+
/>
|
|
49
52
|
</div>
|
|
50
53
|
|
|
51
54
|
{discountAmount !== undefined && discountLabel && (
|
|
@@ -54,6 +57,7 @@ export function InvoiceCheckoutSummary({
|
|
|
54
57
|
{t('ws-invoices.discount')} ({discountLabel})
|
|
55
58
|
</span>
|
|
56
59
|
<FinanceDisplayAmount
|
|
60
|
+
alwaysShow
|
|
57
61
|
className={discountClassName}
|
|
58
62
|
value={`-${formatCurrency(discountAmount, currency)}`}
|
|
59
63
|
/>
|
|
@@ -65,6 +69,7 @@ export function InvoiceCheckoutSummary({
|
|
|
65
69
|
<div className="flex justify-between font-semibold">
|
|
66
70
|
<span>{t('ws-invoices.total')}</span>
|
|
67
71
|
<FinanceDisplayAmount
|
|
72
|
+
alwaysShow
|
|
68
73
|
value={formatCurrency(roundedTotal, currency)}
|
|
69
74
|
/>
|
|
70
75
|
</div>
|
|
@@ -73,6 +78,7 @@ export function InvoiceCheckoutSummary({
|
|
|
73
78
|
<div className="flex justify-between text-muted-foreground text-sm">
|
|
74
79
|
<span>{t('ws-invoices.adjustment')}</span>
|
|
75
80
|
<FinanceDisplayAmount
|
|
81
|
+
alwaysShow
|
|
76
82
|
value={`${roundedTotal > totalBeforeRounding ? '+' : ''}${formatCurrency(
|
|
77
83
|
roundedTotal - totalBeforeRounding,
|
|
78
84
|
currency
|
|
@@ -51,6 +51,7 @@ interface InvoicePaymentSettingsProps {
|
|
|
51
51
|
categoryLabelClassName?: string;
|
|
52
52
|
currency?: string;
|
|
53
53
|
walletDisabled?: boolean;
|
|
54
|
+
walletPermissionWarning?: ReactNode;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function InvoicePaymentSettings({
|
|
@@ -76,6 +77,7 @@ export function InvoicePaymentSettings({
|
|
|
76
77
|
categoryLabelClassName = 'text-dynamic-red',
|
|
77
78
|
currency = 'USD',
|
|
78
79
|
walletDisabled = false,
|
|
80
|
+
walletPermissionWarning,
|
|
79
81
|
}: InvoicePaymentSettingsProps) {
|
|
80
82
|
const t = useTranslations();
|
|
81
83
|
const shouldShowPromotion = showPromotion && promotionsAllowed;
|
|
@@ -168,6 +170,7 @@ export function InvoicePaymentSettings({
|
|
|
168
170
|
))}
|
|
169
171
|
</SelectContent>
|
|
170
172
|
</Select>
|
|
173
|
+
{walletDisabled && walletPermissionWarning}
|
|
171
174
|
</div>
|
|
172
175
|
|
|
173
176
|
<div className="space-y-2">
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle } from '@tuturuuu/icons';
|
|
4
|
+
import { Button } from '@tuturuuu/ui/button';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
6
|
+
import {
|
|
7
|
+
type FinancePermissionRequestUser,
|
|
8
|
+
FinancePermissionWarningDialog,
|
|
9
|
+
} from '../../shared/finance-permission-warning-dialog';
|
|
10
|
+
|
|
11
|
+
interface InvoiceProductsPermissionWarningProps {
|
|
12
|
+
missingPermissions: string[];
|
|
13
|
+
user?: FinancePermissionRequestUser | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function InvoiceProductsPermissionWarning({
|
|
17
|
+
missingPermissions,
|
|
18
|
+
user,
|
|
19
|
+
}: InvoiceProductsPermissionWarningProps) {
|
|
20
|
+
const t = useTranslations();
|
|
21
|
+
const uniqueMissingPermissions = [...new Set(missingPermissions)];
|
|
22
|
+
|
|
23
|
+
if (uniqueMissingPermissions.length === 0) return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="rounded-lg border border-dynamic-orange/30 bg-dynamic-orange/5 p-3">
|
|
27
|
+
<div className="flex items-start gap-3">
|
|
28
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-dynamic-orange" />
|
|
29
|
+
<div className="space-y-3">
|
|
30
|
+
<div className="space-y-1">
|
|
31
|
+
<p className="font-medium text-sm">
|
|
32
|
+
{t('finance-permission-warning.summary')}
|
|
33
|
+
</p>
|
|
34
|
+
<p className="text-muted-foreground text-sm">
|
|
35
|
+
{t('finance-permission-warning.description')}
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
<FinancePermissionWarningDialog
|
|
39
|
+
missingPermissions={uniqueMissingPermissions}
|
|
40
|
+
user={user}
|
|
41
|
+
trigger={
|
|
42
|
+
<Button type="button" variant="outline" size="sm">
|
|
43
|
+
{t('finance-permission-warning.open_request')}
|
|
44
|
+
</Button>
|
|
45
|
+
}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isPermissionRequestError(error: unknown) {
|
|
54
|
+
if (!error || typeof error !== 'object') return false;
|
|
55
|
+
|
|
56
|
+
const status = 'status' in error ? Number(error.status) : 0;
|
|
57
|
+
return status === 401 || status === 403;
|
|
58
|
+
}
|
|
@@ -26,7 +26,8 @@ import { useState } from 'react';
|
|
|
26
26
|
import {
|
|
27
27
|
type GroupPaymentStatus,
|
|
28
28
|
getGroupPaymentStatus,
|
|
29
|
-
|
|
29
|
+
getSubscriptionCoverageInvoiceForGroup,
|
|
30
|
+
isSubscriptionMonthPaidForGroup,
|
|
30
31
|
parseLocalCalendarDate,
|
|
31
32
|
} from '../utils';
|
|
32
33
|
|
|
@@ -47,18 +48,6 @@ function hasSchedule(group: UserGroupItem['workspace_user_groups']): boolean {
|
|
|
47
48
|
return Array.isArray(sessions) && sessions.length > 0;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function isMonthPaidForGroup(
|
|
51
|
-
groupId: string,
|
|
52
|
-
selectedMonth: string,
|
|
53
|
-
latestInvoices: LatestInvoice[]
|
|
54
|
-
): boolean {
|
|
55
|
-
const inv = latestInvoices.find((i) => i.group_id === groupId);
|
|
56
|
-
if (!inv?.valid_until) return false;
|
|
57
|
-
const monthStart = getMonthStartDate(selectedMonth);
|
|
58
|
-
const validUntilStart = getMonthStartDate(inv.valid_until);
|
|
59
|
-
return monthStart < validUntilStart;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
51
|
interface SubscriptionGroupSelectorProps {
|
|
63
52
|
userGroups: UserGroupItem[];
|
|
64
53
|
userGroupsLoading: boolean;
|
|
@@ -267,10 +256,11 @@ export function SubscriptionGroupSelector({
|
|
|
267
256
|
const group = groupItem.workspace_user_groups;
|
|
268
257
|
if (!group) return null;
|
|
269
258
|
const isSelected = selectedGroupIds.includes(group.id);
|
|
270
|
-
const latestInvoice =
|
|
271
|
-
|
|
259
|
+
const latestInvoice = getSubscriptionCoverageInvoiceForGroup(
|
|
260
|
+
latestSubscriptionInvoices,
|
|
261
|
+
group.id
|
|
272
262
|
);
|
|
273
|
-
const isMonthPaid =
|
|
263
|
+
const isMonthPaid = isSubscriptionMonthPaidForGroup(
|
|
274
264
|
group.id,
|
|
275
265
|
selectedMonth,
|
|
276
266
|
latestSubscriptionInvoices
|
|
@@ -312,10 +302,12 @@ export function SubscriptionGroupSelector({
|
|
|
312
302
|
const group = groupItem.workspace_user_groups;
|
|
313
303
|
if (!group) return null;
|
|
314
304
|
const isSelected = selectedGroupIds.includes(group.id);
|
|
315
|
-
const latestInvoice =
|
|
316
|
-
(
|
|
317
|
-
|
|
318
|
-
|
|
305
|
+
const latestInvoice =
|
|
306
|
+
getSubscriptionCoverageInvoiceForGroup(
|
|
307
|
+
latestSubscriptionInvoices,
|
|
308
|
+
group.id
|
|
309
|
+
);
|
|
310
|
+
const isMonthPaid = isSubscriptionMonthPaidForGroup(
|
|
319
311
|
group.id,
|
|
320
312
|
selectedMonth,
|
|
321
313
|
latestSubscriptionInvoices
|
|
@@ -18,10 +18,11 @@ import type { UserGroup } from '../utils';
|
|
|
18
18
|
import {
|
|
19
19
|
getEffectiveAttendanceDays,
|
|
20
20
|
getEffectiveDays,
|
|
21
|
-
getMonthStartDate,
|
|
22
21
|
getSessionsForMonth,
|
|
23
22
|
getSessionsUntilMonth,
|
|
23
|
+
getSubscriptionCoverageInvoiceForGroup,
|
|
24
24
|
getTotalSessionsForGroups,
|
|
25
|
+
isSubscriptionMonthPaidForGroup,
|
|
25
26
|
parseLocalCalendarDate,
|
|
26
27
|
} from '../utils';
|
|
27
28
|
|
|
@@ -174,19 +175,19 @@ const computeGroupAttendanceDaysMap = (
|
|
|
174
175
|
);
|
|
175
176
|
if (!group) continue;
|
|
176
177
|
|
|
177
|
-
const latestInvoice =
|
|
178
|
-
|
|
178
|
+
const latestInvoice = getSubscriptionCoverageInvoiceForGroup(
|
|
179
|
+
latestSubscriptionInvoices,
|
|
180
|
+
groupId
|
|
179
181
|
);
|
|
180
182
|
const validUntil = latestInvoice?.valid_until
|
|
181
183
|
? parseLocalCalendarDate(latestInvoice.valid_until)
|
|
182
184
|
: null;
|
|
183
185
|
|
|
184
|
-
const isGroupPaid = (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
})();
|
|
186
|
+
const isGroupPaid = isSubscriptionMonthPaidForGroup(
|
|
187
|
+
groupId,
|
|
188
|
+
selectedMonth,
|
|
189
|
+
latestSubscriptionInvoices
|
|
190
|
+
);
|
|
190
191
|
|
|
191
192
|
const sessionsArray = group.workspace_user_groups?.sessions || [];
|
|
192
193
|
const groupSessions = isGroupPaid
|