@zeniai/client-epic-state 5.3.12-beta0DI → 5.3.13
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/lib/commonStateTypes/animations.d.ts +1 -1
- package/lib/commonStateTypes/animations.js +1 -0
- package/lib/entity/task/taskPayload.d.ts +0 -1
- package/lib/entity/task/taskPayload.js +0 -1
- package/lib/entity/task/taskReducer.d.ts +1 -4
- package/lib/entity/task/taskReducer.js +2 -19
- package/lib/entity/task/taskState.d.ts +0 -3
- package/lib/epic.d.ts +2 -4
- package/lib/epic.js +2 -9
- package/lib/esm/commonStateTypes/animations.js +1 -0
- package/lib/esm/entity/task/taskPayload.js +0 -1
- package/lib/esm/entity/task/taskReducer.js +1 -18
- package/lib/esm/epic.js +2 -9
- package/lib/esm/index.js +5 -8
- package/lib/esm/reducer.js +0 -6
- package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +65 -3
- package/lib/esm/view/aiCfoView/aiCfoViewSelector.js +25 -0
- package/lib/esm/view/aiCfoView/epics/transcribeAudioEpic.js +60 -0
- package/lib/esm/view/expenseAutomationView/types/transactionGroup.js +10 -5
- package/lib/esm/view/taskManager/taskDetailView/taskDetailSelector.js +0 -3
- package/lib/esm/view/taskManager/taskListView/taskList.js +0 -6
- package/lib/esm/view/taskManager/taskListView/taskListFilterHelpers.js +0 -7
- package/lib/esm/view/taskManager/taskListView/taskListSelector.js +11 -27
- package/lib/esm/view/taskManager/taskListView/taskListViewHelpers.js +0 -37
- package/lib/esm/view/transactionDetail/createTransaction/createTransactionPayload.js +19 -3
- package/lib/esm/view/transactionDetail/createTransaction/createTransactionState.js +2 -0
- package/lib/index.d.ts +6 -10
- package/lib/index.js +28 -37
- package/lib/reducer.d.ts +0 -2
- package/lib/reducer.js +0 -6
- package/lib/rootStateTypes.d.ts +0 -4
- package/lib/view/aiCfoView/aiCfoViewPayload.d.ts +20 -0
- package/lib/view/aiCfoView/aiCfoViewReducer.d.ts +2 -2
- package/lib/view/aiCfoView/aiCfoViewReducer.js +66 -4
- package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +8 -1
- package/lib/view/aiCfoView/aiCfoViewSelector.js +27 -0
- package/lib/view/aiCfoView/aiCfoViewState.d.ts +9 -0
- package/lib/view/aiCfoView/epics/transcribeAudioEpic.d.ts +19 -0
- package/lib/view/aiCfoView/epics/transcribeAudioEpic.js +64 -0
- package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +2 -2
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +2 -2
- package/lib/view/expenseAutomationView/types/transactionGroup.d.ts +6 -6
- package/lib/view/expenseAutomationView/types/transactionGroup.js +10 -5
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +2 -2
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.d.ts +0 -4
- package/lib/view/taskManager/taskDetailView/taskDetailSelector.js +0 -3
- package/lib/view/taskManager/taskListView/taskList.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskList.js +0 -6
- package/lib/view/taskManager/taskListView/taskListFilterHelpers.d.ts +0 -4
- package/lib/view/taskManager/taskListView/taskListFilterHelpers.js +1 -9
- package/lib/view/taskManager/taskListView/taskListSelector.d.ts +3 -6
- package/lib/view/taskManager/taskListView/taskListSelector.js +10 -26
- package/lib/view/taskManager/taskListView/taskListViewHelpers.d.ts +1 -1
- package/lib/view/taskManager/taskListView/taskListViewHelpers.js +0 -37
- package/lib/view/transactionDetail/createTransaction/createTransactionPayload.d.ts +6 -3
- package/lib/view/transactionDetail/createTransaction/createTransactionPayload.js +19 -3
- package/lib/view/transactionDetail/createTransaction/createTransactionState.d.ts +7 -2
- package/lib/view/transactionDetail/createTransaction/createTransactionState.js +2 -0
- package/lib/view/transactionDetail/transactionDetailReducer.d.ts +20 -20
- package/package.json +1 -1
- package/lib/entity/timeEntry/timeEntryPayload.d.ts +0 -26
- package/lib/entity/timeEntry/timeEntryPayload.js +0 -2
- package/lib/entity/timeEntry/timeEntryReducer.d.ts +0 -8
- package/lib/entity/timeEntry/timeEntryReducer.js +0 -76
- package/lib/entity/timeEntry/timeEntrySelector.d.ts +0 -10
- package/lib/entity/timeEntry/timeEntrySelector.js +0 -32
- package/lib/entity/timeEntry/timeEntryState.d.ts +0 -43
- package/lib/entity/timeEntry/timeEntryState.js +0 -13
- package/lib/esm/entity/timeEntry/timeEntryPayload.js +0 -1
- package/lib/esm/entity/timeEntry/timeEntryReducer.js +0 -70
- package/lib/esm/entity/timeEntry/timeEntrySelector.js +0 -23
- package/lib/esm/entity/timeEntry/timeEntryState.js +0 -8
- package/lib/esm/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +0 -24
- package/lib/esm/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +0 -73
- package/lib/esm/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +0 -77
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewPayload.js +0 -1
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewReducer.js +0 -160
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewSelector.js +0 -41
- package/lib/esm/view/taskManager/timeEntryView/timeEntryViewState.js +0 -1
- package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.d.ts +0 -18
- package/lib/view/taskManager/timeEntryView/epic/fetchTaskCategoriesEpic.js +0 -28
- package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.d.ts +0 -23
- package/lib/view/taskManager/timeEntryView/epic/fetchTimeEntriesEpic.js +0 -78
- package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.d.ts +0 -23
- package/lib/view/taskManager/timeEntryView/epic/writeTimeEntryEpic.js +0 -83
- package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.d.ts +0 -27
- package/lib/view/taskManager/timeEntryView/timeEntryViewPayload.js +0 -2
- package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.d.ts +0 -48
- package/lib/view/taskManager/timeEntryView/timeEntryViewReducer.js +0 -164
- package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.d.ts +0 -24
- package/lib/view/taskManager/timeEntryView/timeEntryViewSelector.js +0 -46
- package/lib/view/taskManager/timeEntryView/timeEntryViewState.d.ts +0 -21
- package/lib/view/taskManager/timeEntryView/timeEntryViewState.js +0 -5
|
@@ -47,6 +47,8 @@ export const initialAiCfoViewState = {
|
|
|
47
47
|
fetchChatSessionHistoryByChatSessionId: {},
|
|
48
48
|
responseStateByChatSessionId: {},
|
|
49
49
|
stopSubmitStateByChatSessionId: {},
|
|
50
|
+
transcriptionState: { fetchState: 'Not-Started', error: undefined },
|
|
51
|
+
transcribingChatSessionId: undefined,
|
|
50
52
|
suggestedQuestionsByPageContext: {},
|
|
51
53
|
skillDetail: {
|
|
52
54
|
fetchState: 'Not-Started',
|
|
@@ -147,6 +149,19 @@ function closeSkillsBrowserState(draft) {
|
|
|
147
149
|
draft.uiState.isSkillsBrowserOpen = false;
|
|
148
150
|
clearOpenSkillState(draft);
|
|
149
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Clear a finished dictation attempt when the conversation moves on, so a failed one
|
|
154
|
+
* stops showing "Could not transcribe that recording" in every later chat. A running
|
|
155
|
+
* take keeps its state — the composer is showing a live "Transcribing…" for it.
|
|
156
|
+
*/
|
|
157
|
+
function resetFinishedTranscription(draft) {
|
|
158
|
+
if (draft.transcriptionState.fetchState === 'In-Progress') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
draft.transcriptionState.fetchState = 'Not-Started';
|
|
162
|
+
draft.transcriptionState.error = undefined;
|
|
163
|
+
draft.transcribingChatSessionId = undefined;
|
|
164
|
+
}
|
|
150
165
|
const aiCfoView = createSlice({
|
|
151
166
|
name: 'aiCfoView',
|
|
152
167
|
initialState: initialAiCfoViewState,
|
|
@@ -154,6 +169,7 @@ const aiCfoView = createSlice({
|
|
|
154
169
|
submitQuestion: {
|
|
155
170
|
reducer(draft, action) {
|
|
156
171
|
const { sessionId } = action.payload;
|
|
172
|
+
resetFinishedTranscription(draft);
|
|
157
173
|
// A turn is being sent, so the chat body has to be what the user sees. Doing this
|
|
158
174
|
// here rather than at each call site is deliberate: the composer, the sample
|
|
159
175
|
// chips, Run, and the `/` menu all submit, and every one of them was a separate
|
|
@@ -299,6 +315,7 @@ const aiCfoView = createSlice({
|
|
|
299
315
|
setSession(draft, action) {
|
|
300
316
|
draft.uiState.currentChatSessionId = action.payload.sessionId;
|
|
301
317
|
draft.uiState.agentId = action.payload.agentId;
|
|
318
|
+
resetFinishedTranscription(draft);
|
|
302
319
|
// Picking a session means wanting to see that chat, so the browser cannot stay over
|
|
303
320
|
// it. In the reducer because there are six dispatch sites across three separate
|
|
304
321
|
// rails, and covering them one at a time missed half of them.
|
|
@@ -351,6 +368,7 @@ const aiCfoView = createSlice({
|
|
|
351
368
|
createSessionAndSubmit: {
|
|
352
369
|
reducer(draft, action) {
|
|
353
370
|
draft.uiState.agentId = action.payload.agentId;
|
|
371
|
+
resetFinishedTranscription(draft);
|
|
354
372
|
draft.createSessionAndSubmitState.fetchState = 'In-Progress';
|
|
355
373
|
draft.createSessionAndSubmitState.error = undefined;
|
|
356
374
|
},
|
|
@@ -551,7 +569,49 @@ const aiCfoView = createSlice({
|
|
|
551
569
|
return { payload: { chatMessageId, error } };
|
|
552
570
|
},
|
|
553
571
|
},
|
|
572
|
+
transcribeAudio: {
|
|
573
|
+
reducer(draft, action) {
|
|
574
|
+
draft.transcriptionState.fetchState = 'In-Progress';
|
|
575
|
+
draft.transcriptionState.error = undefined;
|
|
576
|
+
draft.transcribingChatSessionId = action.payload.chatSessionId;
|
|
577
|
+
},
|
|
578
|
+
prepare(audio, language, chatSessionId) {
|
|
579
|
+
return { payload: { audio, chatSessionId, language } };
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
transcribeAudioSuccess: {
|
|
583
|
+
reducer(draft, action) {
|
|
584
|
+
draft.transcriptionState.fetchState = 'Completed';
|
|
585
|
+
draft.transcriptionState.error = undefined;
|
|
586
|
+
// The user moved to another session while this was in flight — dropping it
|
|
587
|
+
// is the only safe option, since appending would put their words in a
|
|
588
|
+
// conversation they did not dictate into.
|
|
589
|
+
if (action.payload.chatSessionId !== draft.uiState.currentChatSessionId) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const transcript = action.payload.transcript.trim();
|
|
593
|
+
if (transcript === '') {
|
|
594
|
+
// Silence, not a failure: leave the composer untouched so the user can
|
|
595
|
+
// simply speak again rather than hunting for what went wrong.
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Append to whatever is in the box now rather than to a snapshot taken when
|
|
599
|
+
// recording began — a take produces exactly one result, so there is nothing
|
|
600
|
+
// to reconcile, and anything typed while transcribing survives.
|
|
601
|
+
const existing = draft.uiState.currentInput.trim();
|
|
602
|
+
draft.uiState.currentInput =
|
|
603
|
+
existing === '' ? transcript : `${existing} ${transcript}`;
|
|
604
|
+
},
|
|
605
|
+
prepare(transcript, chatSessionId) {
|
|
606
|
+
return { payload: { chatSessionId, transcript } };
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
transcribeAudioFailure(draft, action) {
|
|
610
|
+
draft.transcriptionState.fetchState = 'Error';
|
|
611
|
+
draft.transcriptionState.error = action.payload;
|
|
612
|
+
},
|
|
554
613
|
clearCurrentSessionId(draft) {
|
|
614
|
+
resetFinishedTranscription(draft);
|
|
555
615
|
// New Chat: the empty composer is the point, so the browser cannot stay over it.
|
|
556
616
|
if (draft.uiState.isSkillsBrowserOpen === true) {
|
|
557
617
|
closeSkillsBrowserState(draft);
|
|
@@ -835,7 +895,8 @@ const aiCfoView = createSlice({
|
|
|
835
895
|
// The server has it. Nothing it sends from here on will contain this row,
|
|
836
896
|
// so the tombstone has done its job and holding it any longer is what
|
|
837
897
|
// makes a routine recreated under the same id invisible forever.
|
|
838
|
-
draft.routines.deletedScheduleIds =
|
|
898
|
+
draft.routines.deletedScheduleIds =
|
|
899
|
+
draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
|
|
839
900
|
draft.routineMutation = {
|
|
840
901
|
fetchState: 'Completed',
|
|
841
902
|
error: undefined,
|
|
@@ -847,7 +908,8 @@ const aiCfoView = createSlice({
|
|
|
847
908
|
// is what lets the refetch put the row back — leaving it hidden would
|
|
848
909
|
// tell the user they had stopped something they had not, which is the
|
|
849
910
|
// worst outcome this list can produce.
|
|
850
|
-
draft.routines.deletedScheduleIds =
|
|
911
|
+
draft.routines.deletedScheduleIds =
|
|
912
|
+
draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
|
|
851
913
|
// Its own slice: the refetch that puts the row back would otherwise wipe
|
|
852
914
|
// this error, leaving the row silently reappearing with no explanation.
|
|
853
915
|
draft.routineMutation = {
|
|
@@ -1183,5 +1245,5 @@ const aiCfoView = createSlice({
|
|
|
1183
1245
|
});
|
|
1184
1246
|
},
|
|
1185
1247
|
});
|
|
1186
|
-
export const { acceptMasterTOS, acceptMasterTOSFailure, acceptMasterTOSSuccess, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, createSessionAndSubmitFailure, createSessionAndSubmitSuccess, createSessionFailure, createSessionSuccess, deleteChatSession, deleteChatSessionFailure, deleteChatSessionSuccess, deleteRoutine, deleteRoutineFailure, deleteRoutineSuccess, deleteSkill, deleteSkillFailure, deleteSkillSuccess, fetchChatHistory, fetchChatHistoryFailure, fetchChatHistorySuccess, fetchChatSessionsForUser, fetchChatSessionsForUserFailure, fetchChatSessionsForUserSuccess, fetchRoutineRuns, fetchRoutineRunsFailure, fetchRoutineRunsSuccess, fetchRoutines, fetchRoutinesFailure, fetchRoutinesSuccess, fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, initializeNewSessionState, openRoutinesBrowser, openSkillsBrowser, runRoutine, runRoutineFailure, runRoutineSuccess, saveRoutine, saveRoutineFailure, saveRoutineSuccess, saveSkill, saveSkillFailure, saveSkillSuccess, setRoutineEnabled, setSession, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, stopSubmit, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionFailure, stopSubmitQuestionSuccess, stopSubmitSuccess, submitFeedback, submitFeedbackFailure, submitFeedbackSuccess, submitQuestion, submitQuestionFailure, submitQuestionSuccess, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } = aiCfoView.actions;
|
|
1248
|
+
export const { acceptMasterTOS, acceptMasterTOSFailure, acceptMasterTOSSuccess, applyAiCfoSidePanelHostPageTransition, clearAiCfoSidePanelHostPageContext, clearCurrentSessionId, clearDeleteChatSessionStatus, clearLastContextMessage, clearSkillDetail, clearSkillMutation, closeRoutineRuns, closeRoutinesBrowser, closeSkillsBrowser, createSession, createSessionAndSubmit, createSessionAndSubmitFailure, createSessionAndSubmitSuccess, createSessionFailure, createSessionSuccess, deleteChatSession, deleteChatSessionFailure, deleteChatSessionSuccess, deleteRoutine, deleteRoutineFailure, deleteRoutineSuccess, deleteSkill, deleteSkillFailure, deleteSkillSuccess, fetchChatHistory, fetchChatHistoryFailure, fetchChatHistorySuccess, fetchChatSessionsForUser, fetchChatSessionsForUserFailure, fetchChatSessionsForUserSuccess, fetchRoutineRuns, fetchRoutineRunsFailure, fetchRoutineRunsSuccess, fetchRoutines, fetchRoutinesFailure, fetchRoutinesSuccess, fetchSkillDetail, fetchSkillDetailFailure, fetchSkillDetailSuccess, fetchSkills, fetchSkillsFailure, fetchSkillsSuccess, fetchSkillVersionBody, fetchSkillVersionBodyFailure, fetchSkillVersionBodySuccess, fetchSkillVersions, fetchSkillVersionsFailure, fetchSkillVersionsSuccess, fetchSuggestedQuestions, fetchSuggestedQuestionsFailure, fetchSuggestedQuestionsSuccess, initializeNewSessionState, openRoutinesBrowser, openSkillsBrowser, runRoutine, runRoutineFailure, runRoutineSuccess, saveRoutine, saveRoutineFailure, saveRoutineSuccess, saveSkill, saveSkillFailure, saveSkillSuccess, setRoutineEnabled, setSession, setSkillVisibility, setSkillVisibilityFailure, setSkillVisibilitySuccess, stopSubmit, stopSubmitFailure, stopSubmitQuestion, stopSubmitQuestionFailure, stopSubmitQuestionSuccess, stopSubmitSuccess, submitFeedback, submitFeedbackFailure, submitFeedbackSuccess, submitQuestion, submitQuestionFailure, submitQuestionSuccess, transcribeAudio, transcribeAudioFailure, transcribeAudioSuccess, updateAiCfoViewScrollPosition, updateCotCollapsedState, updateCurrentInput, updateResponseState, } = aiCfoView.actions;
|
|
1187
1249
|
export default aiCfoView.reducer;
|
|
@@ -133,3 +133,28 @@ export function getSkillMutation(state) {
|
|
|
133
133
|
export function getSkillsEnabled(state) {
|
|
134
134
|
return getSkills(state).skillsEnabled;
|
|
135
135
|
}
|
|
136
|
+
/** True while a finished voice recording is being transcribed server-side. */
|
|
137
|
+
/**
|
|
138
|
+
* A take belongs to the chat it was dictated into. A new chat has no session id yet on
|
|
139
|
+
* either side, which is a match rather than a mismatch.
|
|
140
|
+
*/
|
|
141
|
+
function isTranscribingCurrentSession(state) {
|
|
142
|
+
if (state.aiCfoViewState == null) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return (state.aiCfoViewState.transcribingChatSessionId ===
|
|
146
|
+
state.aiCfoViewState.uiState.currentChatSessionId);
|
|
147
|
+
}
|
|
148
|
+
export function getIsTranscribing(state) {
|
|
149
|
+
return (state.aiCfoViewState?.transcriptionState?.fetchState === 'In-Progress' &&
|
|
150
|
+
isTranscribingCurrentSession(state));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Set when a transcription request failed. Without surfacing this the composer
|
|
154
|
+
* would just drop the "Transcribing…" label and sit there silently, leaving the
|
|
155
|
+
* user with no sign their recording went nowhere.
|
|
156
|
+
*/
|
|
157
|
+
export function getTranscriptionFailed(state) {
|
|
158
|
+
return (state.aiCfoViewState?.transcriptionState?.fetchState === 'Error' &&
|
|
159
|
+
isTranscribingCurrentSession(state));
|
|
160
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, finalize, map, switchMap, takeUntil, timeout, } from 'rxjs/operators';
|
|
3
|
+
import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
|
|
4
|
+
import { clearAll } from '../../../rootActions';
|
|
5
|
+
import { transcribeAudio, transcribeAudioFailure, transcribeAudioSuccess, } from '../aiCfoViewReducer';
|
|
6
|
+
const AUDIO_FIELD = 'audio';
|
|
7
|
+
const LANGUAGE_FIELD = 'language';
|
|
8
|
+
/** Extension only has to match the recorded format; the server reads the part's MIME type. */
|
|
9
|
+
const AUDIO_FILE_NAME = 'dictation.wav';
|
|
10
|
+
/**
|
|
11
|
+
* A hung request would otherwise pin `isTranscribing` true forever, and the composer
|
|
12
|
+
* gates both submit and the mic on it — leaving the user unable to send even typed
|
|
13
|
+
* text until something else resolved it.
|
|
14
|
+
*/
|
|
15
|
+
const TRANSCRIPTION_TIMEOUT_MS = 30000;
|
|
16
|
+
/**
|
|
17
|
+
* Voice dictation for the AI CFO composer. Dispatch `transcribeAudio(blob, language)` with a
|
|
18
|
+
* finished recording to POST it to the AI CFO micro-service `/1.0/transcriptions` endpoint;
|
|
19
|
+
* on success the transcript is appended to the composer input for the user to edit and send.
|
|
20
|
+
*
|
|
21
|
+
* `switchMap` (not `mergeMap`): a newer recording supersedes an older in-flight one, so a
|
|
22
|
+
* slow response can never append a stale transcript after a fresh one has already landed.
|
|
23
|
+
* The AbortController makes that cancellation real — `fetch` wrapped in a promise ignores
|
|
24
|
+
* an RxJS unsubscribe, so without the signal the superseded request would run to completion
|
|
25
|
+
* and still bill a transcription call whose result nothing wants.
|
|
26
|
+
*/
|
|
27
|
+
export const transcribeAudioEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(transcribeAudio.match), switchMap((action) => {
|
|
28
|
+
const { audio, chatSessionId, language } = action.payload;
|
|
29
|
+
const abortController = new AbortController();
|
|
30
|
+
const formData = new FormData();
|
|
31
|
+
formData.append(AUDIO_FIELD, audio, AUDIO_FILE_NAME);
|
|
32
|
+
if (language != null && language !== '') {
|
|
33
|
+
formData.append(LANGUAGE_FIELD, language);
|
|
34
|
+
}
|
|
35
|
+
return zeniAPI
|
|
36
|
+
.postFormData(`${zeniAPI.apiEndPoints.aiCfoMicroServiceBaseUrl}/1.0/transcriptions`, formData, abortController.signal)
|
|
37
|
+
.pipe(timeout(TRANSCRIPTION_TIMEOUT_MS),
|
|
38
|
+
// A tenant switch clears the store; a transcript still in flight must not
|
|
39
|
+
// land in the next tenant's composer (both sessions can be `undefined`, so
|
|
40
|
+
// the reducer's session guard would let it through).
|
|
41
|
+
takeUntil(actions$.pipe(filter(clearAll.match))), map((response) => {
|
|
42
|
+
if (!isSuccessResponse(response)) {
|
|
43
|
+
return transcribeAudioFailure(response.status);
|
|
44
|
+
}
|
|
45
|
+
// An EMPTY transcript is a success (the mic caught silence) and the
|
|
46
|
+
// reducer leaves the composer alone. A MISSING one breaks the response
|
|
47
|
+
// contract, so it surfaces as an error rather than being coerced to
|
|
48
|
+
// silence the user cannot distinguish from a quiet room.
|
|
49
|
+
if (response.data?.transcript == null) {
|
|
50
|
+
return transcribeAudioFailure(createZeniAPIStatus('Transcription succeeded but the response carried no transcript', 'Unexpected Error'));
|
|
51
|
+
}
|
|
52
|
+
// The session id rides along so a transcript that lands after the user
|
|
53
|
+
// switched sessions is dropped rather than misfiled.
|
|
54
|
+
return transcribeAudioSuccess(response.data.transcript, chatSessionId);
|
|
55
|
+
}), finalize(() => abortController.abort()), catchError((error) => of(transcribeAudioFailure(createZeniAPIStatus(
|
|
56
|
+
// message (detailed) first, then the short title — per createZeniAPIStatus(message, title).
|
|
57
|
+
// JSON.stringify(Error) drops the (non-enumerable) message — extract it explicitly.
|
|
58
|
+
'Transcription REST API call errored out: ' +
|
|
59
|
+
(error instanceof Error ? error.message : String(error)), 'Unexpected Error')))));
|
|
60
|
+
}));
|
|
@@ -4,15 +4,20 @@ import { stringToUnion } from '../../../commonStateTypes/stringToUnion';
|
|
|
4
4
|
* verbatim to the listing API as `transaction_group`. When revenue automation is
|
|
5
5
|
* enabled for the tenant the backend scopes the whole view by the line
|
|
6
6
|
* account_type: `'expense'` -> expense accounts, `'income'` -> income accounts,
|
|
7
|
-
* `'
|
|
8
|
-
*
|
|
7
|
+
* `'other'` -> balance-sheet accounts other than bank/credit card (plus lines
|
|
8
|
+
* with no account type), `'all'` -> all three. The list epics always send it;
|
|
9
|
+
* when the gate is off the backend ignores it and returns the legacy expense
|
|
10
|
+
* scope.
|
|
9
11
|
*
|
|
10
12
|
* The wire value for expenses is the singular `'expense'` (matches the backend
|
|
11
13
|
* `TransactionGroup` enum) even though the picker labels it "Expenses".
|
|
12
|
-
* Balance-sheet groups (assets, liability) are reserved for later and are not
|
|
13
|
-
* part of this union yet.
|
|
14
14
|
*/
|
|
15
|
-
export const TRANSACTION_GROUPS = [
|
|
15
|
+
export const TRANSACTION_GROUPS = [
|
|
16
|
+
'all',
|
|
17
|
+
'income',
|
|
18
|
+
'expense',
|
|
19
|
+
'other',
|
|
20
|
+
];
|
|
16
21
|
export const toTransactionGroup = (v) => stringToUnion(v != null ? v.trim().toLowerCase() : '', TRANSACTION_GROUPS);
|
|
17
22
|
/**
|
|
18
23
|
* Default group for the listing — matches the picker's default selection ("All").
|
|
@@ -26,7 +26,6 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
26
26
|
let isDeleted = undefined;
|
|
27
27
|
let depth = undefined;
|
|
28
28
|
let recurringSourceTaskId = undefined;
|
|
29
|
-
let taskCategoryId = undefined;
|
|
30
29
|
let snoozedUntil = undefined;
|
|
31
30
|
let taskGroupId = undefined;
|
|
32
31
|
let subtasks = [];
|
|
@@ -40,7 +39,6 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
40
39
|
if (taskEntity != null) {
|
|
41
40
|
createdByUser = getUserAndUserRole(userState, userRoleState, addressState, taskEntity.createdBy);
|
|
42
41
|
depth = taskEntity.depth;
|
|
43
|
-
taskCategoryId = taskEntity.taskCategoryId;
|
|
44
42
|
isArchived = taskEntity.isArchived;
|
|
45
43
|
isDeleted = taskEntity.isDeleted;
|
|
46
44
|
recurringSourceTaskId = taskEntity.recurringSourceTaskId;
|
|
@@ -105,7 +103,6 @@ export const getTaskDetail = (state, taskId) => {
|
|
|
105
103
|
version: 0,
|
|
106
104
|
createdByUser,
|
|
107
105
|
depth,
|
|
108
|
-
taskCategoryId,
|
|
109
106
|
isArchived,
|
|
110
107
|
isDeleted,
|
|
111
108
|
recurringSourceTaskId,
|
|
@@ -47,13 +47,7 @@ export const TASK_LIST_FILTER_CATEGORIES = [
|
|
|
47
47
|
type: 'user',
|
|
48
48
|
multiple: true,
|
|
49
49
|
},
|
|
50
|
-
{
|
|
51
|
-
value: 'createdBy',
|
|
52
|
-
type: 'user',
|
|
53
|
-
multiple: true,
|
|
54
|
-
},
|
|
55
50
|
{ value: 'creationDate', type: 'dateRange', multiple: true },
|
|
56
|
-
{ value: 'billing', type: 'dropdown', multiple: true },
|
|
57
51
|
];
|
|
58
52
|
export const TASK_LIST_GROUP_BY_CATEGORIES = [
|
|
59
53
|
{ value: 'status' },
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/** Billing lives on the task's category, not the task, so it needs the catalog. */
|
|
2
|
-
export const buildCategoryValueGetter = (taskCategoriesById) => (key, task) => key === 'billing'
|
|
3
|
-
? (taskCategoriesById[task.taskCategoryId ?? '']?.billingType ??
|
|
4
|
-
'non_billable')
|
|
5
|
-
: getCategoryValueForTaskList(key, task);
|
|
6
1
|
export const getCategoryValueForTaskList = (key, task) => {
|
|
7
2
|
switch (key) {
|
|
8
3
|
case 'dueDate':
|
|
@@ -17,8 +12,6 @@ export const getCategoryValueForTaskList = (key, task) => {
|
|
|
17
12
|
return task.createTime;
|
|
18
13
|
case 'assignee':
|
|
19
14
|
return task.assignees;
|
|
20
|
-
case 'createdBy':
|
|
21
|
-
return task.createdBy;
|
|
22
15
|
default:
|
|
23
16
|
return undefined;
|
|
24
17
|
}
|
|
@@ -15,7 +15,7 @@ import { getUserGroupList } from '../../userGroupListView/userGroupListViewSelec
|
|
|
15
15
|
import { getUserList, } from '../../userListView/userListViewSelector';
|
|
16
16
|
import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
|
|
17
17
|
import { DUE_DATE_GROUP_KEYS, } from './taskList';
|
|
18
|
-
import {
|
|
18
|
+
import { getCategoryValueForTaskList } from './taskListFilterHelpers';
|
|
19
19
|
const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
|
|
20
20
|
const tasksInOrder = orderBy(tasksList, (task) => {
|
|
21
21
|
switch (sortKey) {
|
|
@@ -47,7 +47,7 @@ const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
|
|
|
47
47
|
}, sortOrder);
|
|
48
48
|
return tasksInOrder;
|
|
49
49
|
};
|
|
50
|
-
const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState,
|
|
50
|
+
const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, userGroupsState, }) => {
|
|
51
51
|
const { sortKey, searchText } = taskListState.uiState;
|
|
52
52
|
const sortOrder = getSortOrder(taskListState.uiState.sortOrder);
|
|
53
53
|
const tasks = taskIds
|
|
@@ -70,11 +70,11 @@ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRole
|
|
|
70
70
|
return undefined;
|
|
71
71
|
})
|
|
72
72
|
.filter((value) => value?.name.toLowerCase().includes(searchText.toLowerCase()) === true);
|
|
73
|
-
const filteredTaskList = applyAdvancedFiltersOnList(
|
|
73
|
+
const filteredTaskList = applyAdvancedFiltersOnList(getCategoryValueForTaskList, tasks, taskListState.filters);
|
|
74
74
|
const sortedTaskList = sortTasksList(tagState, filteredTaskList, sortKey, sortOrder);
|
|
75
75
|
return sortedTaskList;
|
|
76
76
|
};
|
|
77
|
-
const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState,
|
|
77
|
+
const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupState, taskGroupsIds, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
78
78
|
const taskGroupsWithTasksList = taskGroupsIds
|
|
79
79
|
.map((taskGroupId) => {
|
|
80
80
|
const taskGroup = getTaskGroupById(taskGroupState, taskGroupId);
|
|
@@ -88,7 +88,6 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
|
|
|
88
88
|
taskIds,
|
|
89
89
|
tagState,
|
|
90
90
|
userGroupsState,
|
|
91
|
-
taskCategoriesById,
|
|
92
91
|
});
|
|
93
92
|
if (taskGroup != null) {
|
|
94
93
|
return {
|
|
@@ -120,7 +119,7 @@ const getTaskListGroupedByGroupName = ({ taskListState, taskState, taskGroupStat
|
|
|
120
119
|
// Also hides deleted groups, which are empty as a side effect.
|
|
121
120
|
return sortedTaskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
122
121
|
};
|
|
123
|
-
export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState,
|
|
122
|
+
export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
124
123
|
const assigneeGroups = Object.keys(taskListState.taskIdsByAssignees);
|
|
125
124
|
const taskGroupsWithTasksList = assigneeGroups.map((groupKey) => {
|
|
126
125
|
const taskIds = taskListState.taskIdsByAssignees[groupKey];
|
|
@@ -133,7 +132,6 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
|
|
|
133
132
|
taskIds,
|
|
134
133
|
tagState,
|
|
135
134
|
userGroupsState,
|
|
136
|
-
taskCategoriesById,
|
|
137
135
|
});
|
|
138
136
|
const assigneeIds = groupKey.split(',');
|
|
139
137
|
const groupName = getUsersByUserIds(userState, assigneeIds)
|
|
@@ -151,7 +149,7 @@ export const getTaskListGroupedByAssignees = ({ taskListState, taskState, userSt
|
|
|
151
149
|
});
|
|
152
150
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
153
151
|
};
|
|
154
|
-
export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState,
|
|
152
|
+
export const getTaskListGroupedByPriority = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
155
153
|
const taskGroupsWithTasksList = ALL_PRIORITY_CODE.map((groupKey) => {
|
|
156
154
|
const taskIds = taskListState.taskIdsByPriority[groupKey] ?? [];
|
|
157
155
|
const tasksWithUserDetails = getTasksWithUserDetails({
|
|
@@ -163,7 +161,6 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
|
|
|
163
161
|
taskIds,
|
|
164
162
|
tagState,
|
|
165
163
|
userGroupsState,
|
|
166
|
-
taskCategoriesById,
|
|
167
164
|
});
|
|
168
165
|
return {
|
|
169
166
|
groupName: allTaskPriority.find((priority) => priority.code === groupKey)?.name ??
|
|
@@ -175,7 +172,7 @@ export const getTaskListGroupedByPriority = ({ taskListState, taskState, userSta
|
|
|
175
172
|
});
|
|
176
173
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
177
174
|
};
|
|
178
|
-
const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState,
|
|
175
|
+
const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
179
176
|
const taskGroupsWithTasksList = ALL_TASK_STATUS_CODE.map((groupKey) => {
|
|
180
177
|
const taskIds = taskListState.taskIdsByStatus[groupKey] ?? [];
|
|
181
178
|
const tasksWithUserDetails = getTasksWithUserDetails({
|
|
@@ -187,7 +184,6 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
|
|
|
187
184
|
taskIds,
|
|
188
185
|
tagState,
|
|
189
186
|
userGroupsState,
|
|
190
|
-
taskCategoriesById,
|
|
191
187
|
});
|
|
192
188
|
return {
|
|
193
189
|
groupName: allTaskStatus.find((taskStatus) => taskStatus.code === groupKey)
|
|
@@ -199,7 +195,7 @@ const getTaskListGroupedByStatus = ({ taskListState, taskState, userState, userR
|
|
|
199
195
|
});
|
|
200
196
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
201
197
|
};
|
|
202
|
-
const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState,
|
|
198
|
+
const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
203
199
|
const tagGroups = Object.keys(taskListState.taskIdsByTags);
|
|
204
200
|
const taskGroupsWithTasksList = tagGroups.map((groupKey) => {
|
|
205
201
|
const taskIds = taskListState.taskIdsByTags[groupKey];
|
|
@@ -212,7 +208,6 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
|
|
|
212
208
|
taskIds,
|
|
213
209
|
tagState,
|
|
214
210
|
userGroupsState,
|
|
215
|
-
taskCategoriesById,
|
|
216
211
|
});
|
|
217
212
|
const tagIds = groupKey.split(',');
|
|
218
213
|
const groupName = getTagsByIds(tagState, tagIds)
|
|
@@ -229,7 +224,7 @@ const getTaskListGroupedByTags = ({ taskListState, taskState, userState, userRol
|
|
|
229
224
|
});
|
|
230
225
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
231
226
|
};
|
|
232
|
-
const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState,
|
|
227
|
+
const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, userRoleState, addressState, tagState, userGroupsState, }) => {
|
|
233
228
|
const taskGroupsWithTasksList = DUE_DATE_GROUP_KEYS.map((groupKey) => {
|
|
234
229
|
const taskIds = taskListState.taskIdsByDueDate[groupKey];
|
|
235
230
|
const tasksWithUserDetails = getTasksWithUserDetails({
|
|
@@ -241,7 +236,6 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
|
|
|
241
236
|
taskIds,
|
|
242
237
|
tagState,
|
|
243
238
|
userGroupsState,
|
|
244
|
-
taskCategoriesById,
|
|
245
239
|
});
|
|
246
240
|
return {
|
|
247
241
|
groupName: groupKey,
|
|
@@ -252,7 +246,7 @@ const getTaskListGroupedByDueDate = ({ taskListState, taskState, userState, user
|
|
|
252
246
|
});
|
|
253
247
|
return taskGroupsWithTasksList.filter((group) => group.tasks.length > 0);
|
|
254
248
|
};
|
|
255
|
-
const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState,
|
|
249
|
+
const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, }) => {
|
|
256
250
|
const { groupByKey } = taskListState.uiState;
|
|
257
251
|
if (groupByKey === 'groupName') {
|
|
258
252
|
return getTaskListGroupedByGroupName({
|
|
@@ -265,7 +259,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
265
259
|
taskGroupsIds,
|
|
266
260
|
tagState,
|
|
267
261
|
userGroupsState,
|
|
268
|
-
taskCategoriesById,
|
|
269
262
|
});
|
|
270
263
|
}
|
|
271
264
|
if (groupByKey === 'assignee') {
|
|
@@ -277,7 +270,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
277
270
|
addressState,
|
|
278
271
|
tagState,
|
|
279
272
|
userGroupsState,
|
|
280
|
-
taskCategoriesById,
|
|
281
273
|
});
|
|
282
274
|
}
|
|
283
275
|
if (groupByKey === 'priority') {
|
|
@@ -289,7 +281,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
289
281
|
addressState,
|
|
290
282
|
tagState,
|
|
291
283
|
userGroupsState,
|
|
292
|
-
taskCategoriesById,
|
|
293
284
|
});
|
|
294
285
|
}
|
|
295
286
|
if (groupByKey === 'status') {
|
|
@@ -301,7 +292,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
301
292
|
addressState,
|
|
302
293
|
tagState,
|
|
303
294
|
userGroupsState,
|
|
304
|
-
taskCategoriesById,
|
|
305
295
|
});
|
|
306
296
|
}
|
|
307
297
|
if (groupByKey === 'tag') {
|
|
@@ -313,7 +303,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
313
303
|
addressState,
|
|
314
304
|
tagState,
|
|
315
305
|
userGroupsState,
|
|
316
|
-
taskCategoriesById,
|
|
317
306
|
});
|
|
318
307
|
}
|
|
319
308
|
return getTaskListGroupedByDueDate({
|
|
@@ -324,7 +313,6 @@ const getTaskGroupsWithTaskList = ({ taskListState, taskState, userState, userRo
|
|
|
324
313
|
addressState,
|
|
325
314
|
tagState,
|
|
326
315
|
userGroupsState,
|
|
327
|
-
taskCategoriesById,
|
|
328
316
|
});
|
|
329
317
|
};
|
|
330
318
|
/**
|
|
@@ -349,10 +337,7 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
|
|
|
349
337
|
// invalidate this memo and rebuild a board the user is already using.
|
|
350
338
|
(state) => tagsAreStructural(state.taskListState.uiState)
|
|
351
339
|
? state.tagState
|
|
352
|
-
: EMPTY_TAG_STATE, (state) => state.userGroupsState,
|
|
353
|
-
// Only the catalog, not the whole timeEntry slice: billing is read off
|
|
354
|
-
// the task's category, and a timer tick must not rebuild the board.
|
|
355
|
-
(state) => state.timeEntryState.taskCategoriesById, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState, taskCategoriesById) => getTaskGroupsWithTaskList({
|
|
340
|
+
: EMPTY_TAG_STATE, (state) => state.userGroupsState, (uiState, filters, taskIdsByGroupsIds, taskIdsByAssignees, taskIdsByPriority, taskIdsByStatus, taskIdsByTags, taskIdsByDueDate, taskState, userState, userRoleState, addressState, taskGroupState, taskGroupsIds, tagState, userGroupsState) => getTaskGroupsWithTaskList({
|
|
356
341
|
taskListState: {
|
|
357
342
|
uiState,
|
|
358
343
|
filters,
|
|
@@ -371,7 +356,6 @@ const getTaskGroupsWithTaskListMemoized = createSelector((state) => state.taskLi
|
|
|
371
356
|
taskGroupsIds,
|
|
372
357
|
tagState,
|
|
373
358
|
userGroupsState,
|
|
374
|
-
taskCategoriesById,
|
|
375
359
|
}));
|
|
376
360
|
export const getAllTasks = createSelector(
|
|
377
361
|
// `taskState`, `taskGroupState`, `addressState` and `tagState` are gone from
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { convertHHMMStrToMinutes } from '../../../commonStateTypes/fiscalYearHelpers/formatMinutesToFromHHMM';
|
|
2
1
|
import { dateNow } from '../../../zeniDayJS';
|
|
3
2
|
import { allTaskPriority, allTaskStatus, } from '../taskDetailView/taskDetailSelector';
|
|
4
3
|
import { toDueDateGroupKeyType } from './taskList';
|
|
@@ -64,11 +63,6 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
|
|
|
64
63
|
return acc;
|
|
65
64
|
}, {});
|
|
66
65
|
const unknownStatusRank = allTaskStatus.length;
|
|
67
|
-
const priorityOrder = allTaskPriority.reduce((acc, priority, index) => {
|
|
68
|
-
acc[priority.code] = index;
|
|
69
|
-
return acc;
|
|
70
|
-
}, {});
|
|
71
|
-
const unknownPriorityRank = allTaskPriority.length;
|
|
72
66
|
const cmpStr = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
|
|
73
67
|
const keyFn = (task) => {
|
|
74
68
|
if (sortKey === 'name') {
|
|
@@ -95,37 +89,6 @@ export const sortSubtasks = (subtasks, sortKey, sortOrder, userList, tagList) =>
|
|
|
95
89
|
const tag = tagList.find((candidate) => candidate.tagId === firstTagId);
|
|
96
90
|
return tag?.name ?? '';
|
|
97
91
|
}
|
|
98
|
-
if (sortKey === 'priority') {
|
|
99
|
-
// Rank by the canonical order, not the code string: alphabetical would
|
|
100
|
-
// sort "high" before "urgent" and read as wrong to anyone scanning it.
|
|
101
|
-
return priorityOrder[task.priority?.code] ?? unknownPriorityRank;
|
|
102
|
-
}
|
|
103
|
-
if (sortKey === 'dueDate') {
|
|
104
|
-
// A task with no date is not "earliest", so undated rows belong at the
|
|
105
|
-
// bottom in BOTH directions. The comparator negates for 'desc', so one
|
|
106
|
-
// fixed sentinel would flip them to the top — the sentinel has to flip
|
|
107
|
-
// with the order to survive that negation.
|
|
108
|
-
// `dueDate` is already a ZeniDate (dayjs), so valueOf() is the epoch.
|
|
109
|
-
// An unparseable date yields NaN, and a NaN comparator leaves the array
|
|
110
|
-
// untouched -- the same silent failure the timeSpent branch guards.
|
|
111
|
-
// Treat it as undated rather than as a date.
|
|
112
|
-
const due = task.dueDate;
|
|
113
|
-
const epoch = due?.valueOf();
|
|
114
|
-
if (epoch == null || Number.isNaN(epoch)) {
|
|
115
|
-
return sortOrder === 'asc'
|
|
116
|
-
? Number.MAX_SAFE_INTEGER
|
|
117
|
-
: Number.MIN_SAFE_INTEGER;
|
|
118
|
-
}
|
|
119
|
-
return epoch;
|
|
120
|
-
}
|
|
121
|
-
if (sortKey === 'timeSpent') {
|
|
122
|
-
// `timeSpent` is an "HHMM" STRING (no colon). Comparing it as text puts
|
|
123
|
-
// "1000" before "0900" only by luck of padding, and any malformed value
|
|
124
|
-
// makes the converter return NaN -- which a comparator turns into "no
|
|
125
|
-
// sort at all", silently. Coerce that to 0.
|
|
126
|
-
const minutes = convertHHMMStrToMinutes(task.timeSpent ?? '');
|
|
127
|
-
return Number.isFinite(minutes) ? minutes : 0;
|
|
128
|
-
}
|
|
129
92
|
return '';
|
|
130
93
|
};
|
|
131
94
|
const copy = [...subtasks];
|
|
@@ -131,13 +131,29 @@ export const prepareCreateTransactionPayload = (state, localData) => {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
const lines = localData.lines.map((line) => toLinePayload(state, line, transactionType));
|
|
134
|
+
const documentIdField = localData.documentNumber != null && localData.documentNumber !== ''
|
|
135
|
+
? { document_id: localData.documentNumber }
|
|
136
|
+
: {};
|
|
134
137
|
if (transactionType === 'journal_entry') {
|
|
138
|
+
return { ...base, ...documentIdField, lines };
|
|
139
|
+
}
|
|
140
|
+
// Both post to accounts payable, so they carry no header account or payment type.
|
|
141
|
+
if (transactionType === 'vendor_credit') {
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
vendor_id: toQboVendorId(state, localData.vendorId),
|
|
145
|
+
...documentIdField,
|
|
146
|
+
lines,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (transactionType === 'bill') {
|
|
135
150
|
return {
|
|
136
151
|
...base,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
vendor_id: toQboVendorId(state, localData.vendorId),
|
|
153
|
+
...(localData.dueDate != null && {
|
|
154
|
+
due_date: localData.dueDate.format(DEFAULT_DATE_FORMAT),
|
|
140
155
|
}),
|
|
156
|
+
...documentIdField,
|
|
141
157
|
lines,
|
|
142
158
|
};
|
|
143
159
|
}
|