Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
659
static/js/modules/composables/modals.js
Normal file
659
static/js/modules/composables/modals.js
Normal file
@@ -0,0 +1,659 @@
|
||||
/**
|
||||
* Modal management composable
|
||||
* Handles opening, closing, and saving modal dialogs
|
||||
*/
|
||||
|
||||
export function useModals(state, utils) {
|
||||
const {
|
||||
showEditModal, showDeleteModal, showEditTagsModal,
|
||||
showReprocessModal, showResetModal, showShareModal,
|
||||
showSharesListModal, showTextEditorModal, showAsrEditorModal,
|
||||
showEditSpeakersModal, showAddSpeakerModal, showEditTextModal,
|
||||
showShareDeleteModal, showUnifiedShareModal, showColorSchemeModal,
|
||||
showSystemAudioHelpModal, editingRecording, recordingToDelete, recordingToReset,
|
||||
selectedRecording, recordings, selectedNewTagId, tagSearchFilter,
|
||||
availableTags, currentView, totalRecordings, toasts, uploadQueue, allJobs,
|
||||
// DateTime picker state
|
||||
showDateTimePicker, pickerMonth, pickerYear, pickerHour, pickerMinute,
|
||||
pickerAmPm, pickerSelectedDate, dateTimePickerTarget, dateTimePickerCallback
|
||||
} = state;
|
||||
|
||||
const { showToast, setGlobalError } = utils;
|
||||
const { computed } = Vue;
|
||||
|
||||
// =========================================
|
||||
// Edit Recording Modal
|
||||
// =========================================
|
||||
|
||||
const openEditModal = (recording) => {
|
||||
editingRecording.value = { ...recording };
|
||||
showEditModal.value = true;
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
showEditModal.value = false;
|
||||
editingRecording.value = null;
|
||||
};
|
||||
|
||||
const saveEdit = async () => {
|
||||
if (!editingRecording.value) return;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/api/recordings/${editingRecording.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: editingRecording.value.title,
|
||||
participants: editingRecording.value.participants,
|
||||
meeting_date: editingRecording.value.meeting_date,
|
||||
notes: editingRecording.value.notes
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to save changes');
|
||||
|
||||
// Update local data
|
||||
const index = recordings.value.findIndex(r => r.id === editingRecording.value.id);
|
||||
if (index !== -1) {
|
||||
recordings.value[index] = { ...recordings.value[index], ...editingRecording.value };
|
||||
}
|
||||
if (selectedRecording.value && selectedRecording.value.id === editingRecording.value.id) {
|
||||
selectedRecording.value = { ...selectedRecording.value, ...editingRecording.value };
|
||||
}
|
||||
|
||||
showToast('Recording updated!', 'fa-check-circle');
|
||||
showEditModal.value = false;
|
||||
editingRecording.value = null;
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to save changes: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Delete Recording Modal
|
||||
// =========================================
|
||||
|
||||
const confirmDelete = (recording) => {
|
||||
recordingToDelete.value = recording;
|
||||
showDeleteModal.value = true;
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
showDeleteModal.value = false;
|
||||
recordingToDelete.value = null;
|
||||
};
|
||||
|
||||
const deleteRecording = async () => {
|
||||
if (!recordingToDelete.value) return;
|
||||
const deletedId = recordingToDelete.value.id;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/recording/${deletedId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRFToken': csrfToken }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to delete recording');
|
||||
|
||||
// Remove from recordings list
|
||||
recordings.value = recordings.value.filter(r => r.id !== deletedId);
|
||||
totalRecordings.value--;
|
||||
|
||||
// Remove from upload queue if present (frontend tracking)
|
||||
if (uploadQueue?.value) {
|
||||
uploadQueue.value = uploadQueue.value.filter(item => item.recordingId !== deletedId);
|
||||
}
|
||||
|
||||
// Remove from backend job queue if present (backend processing tracking)
|
||||
// This is critical - without this, deleted recordings remain in processing queue
|
||||
if (allJobs?.value) {
|
||||
allJobs.value = allJobs.value.filter(job => job.recording_id !== deletedId);
|
||||
}
|
||||
|
||||
// Clear selected recording if it's the one being deleted
|
||||
if (selectedRecording.value?.id === deletedId) {
|
||||
selectedRecording.value = null;
|
||||
currentView.value = 'upload';
|
||||
}
|
||||
|
||||
showToast('Recording deleted.', 'fa-trash');
|
||||
showDeleteModal.value = false;
|
||||
recordingToDelete.value = null;
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to delete recording: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Archive Recording
|
||||
// =========================================
|
||||
|
||||
const archiveRecording = async (recording) => {
|
||||
if (!recording) return;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/api/recordings/${recording.id}/archive`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': csrfToken }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to archive recording');
|
||||
|
||||
recording.is_archived = true;
|
||||
recording.audio_deleted_at = data.audio_deleted_at;
|
||||
|
||||
// Update in recordings list
|
||||
const index = recordings.value.findIndex(r => r.id === recording.id);
|
||||
if (index !== -1) {
|
||||
recordings.value[index].is_archived = true;
|
||||
recordings.value[index].audio_deleted_at = data.audio_deleted_at;
|
||||
}
|
||||
|
||||
showToast('Recording archived (audio deleted)', 'fa-archive');
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to archive recording: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Edit Tags Modal
|
||||
// =========================================
|
||||
|
||||
const openEditTagsModal = () => {
|
||||
selectedNewTagId.value = '';
|
||||
tagSearchFilter.value = '';
|
||||
showEditTagsModal.value = true;
|
||||
};
|
||||
|
||||
const closeEditTagsModal = () => {
|
||||
showEditTagsModal.value = false;
|
||||
};
|
||||
|
||||
const addTagToRecording = async (tagId) => {
|
||||
if (!selectedRecording.value || !tagId) return;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/api/recordings/${selectedRecording.value.id}/tags`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ tag_id: tagId })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to add tag');
|
||||
|
||||
// Find the tag object
|
||||
const tag = availableTags.value.find(t => t.id === tagId);
|
||||
if (tag) {
|
||||
if (!selectedRecording.value.tags) {
|
||||
selectedRecording.value.tags = [];
|
||||
}
|
||||
selectedRecording.value.tags.push(tag);
|
||||
}
|
||||
|
||||
// Update in recordings list
|
||||
const index = recordings.value.findIndex(r => r.id === selectedRecording.value.id);
|
||||
if (index !== -1 && tag) {
|
||||
if (!recordings.value[index].tags) {
|
||||
recordings.value[index].tags = [];
|
||||
}
|
||||
recordings.value[index].tags.push(tag);
|
||||
}
|
||||
|
||||
selectedNewTagId.value = '';
|
||||
showToast('Tag added!', 'fa-tag');
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to add tag: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTagFromRecording = async (tagId) => {
|
||||
if (!selectedRecording.value || !tagId) return;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/api/recordings/${selectedRecording.value.id}/tags/${tagId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRFToken': csrfToken }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to remove tag');
|
||||
|
||||
// Remove from selected recording
|
||||
if (selectedRecording.value.tags) {
|
||||
selectedRecording.value.tags = selectedRecording.value.tags.filter(t => t.id !== tagId);
|
||||
}
|
||||
|
||||
// Update in recordings list
|
||||
const index = recordings.value.findIndex(r => r.id === selectedRecording.value.id);
|
||||
if (index !== -1 && recordings.value[index].tags) {
|
||||
recordings.value[index].tags = recordings.value[index].tags.filter(t => t.id !== tagId);
|
||||
}
|
||||
|
||||
showToast('Tag removed!', 'fa-tag');
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to remove tag: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Reset Modal
|
||||
// =========================================
|
||||
|
||||
const openResetModal = (recording) => {
|
||||
recordingToReset.value = recording;
|
||||
showResetModal.value = true;
|
||||
};
|
||||
|
||||
const cancelReset = () => {
|
||||
showResetModal.value = false;
|
||||
recordingToReset.value = null;
|
||||
};
|
||||
|
||||
const resetRecording = async () => {
|
||||
if (!recordingToReset.value) return;
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/recording/${recordingToReset.value.id}/reset_status`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': csrfToken }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to reset recording');
|
||||
|
||||
// Update recording status
|
||||
const index = recordings.value.findIndex(r => r.id === recordingToReset.value.id);
|
||||
if (index !== -1) {
|
||||
recordings.value[index].status = 'PENDING';
|
||||
recordings.value[index].transcription = '';
|
||||
recordings.value[index].summary = '';
|
||||
}
|
||||
|
||||
if (selectedRecording.value?.id === recordingToReset.value.id) {
|
||||
selectedRecording.value.status = 'PENDING';
|
||||
selectedRecording.value.transcription = '';
|
||||
selectedRecording.value.summary = '';
|
||||
}
|
||||
|
||||
showToast('Recording reset for reprocessing.', 'fa-redo');
|
||||
showResetModal.value = false;
|
||||
recordingToReset.value = null;
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to reset recording: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// System Audio Help Modal
|
||||
// =========================================
|
||||
|
||||
const openSystemAudioHelpModal = () => {
|
||||
showSystemAudioHelpModal.value = true;
|
||||
};
|
||||
|
||||
const closeSystemAudioHelpModal = () => {
|
||||
showSystemAudioHelpModal.value = false;
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Toast Management
|
||||
// =========================================
|
||||
|
||||
const dismissToast = (id) => {
|
||||
toasts.value = toasts.value.filter(t => t.id !== id);
|
||||
};
|
||||
|
||||
// Aliases for template compatibility
|
||||
const editRecording = openEditModal;
|
||||
const editRecordingTags = openEditTagsModal;
|
||||
|
||||
// =========================================
|
||||
// DateTime Picker
|
||||
// =========================================
|
||||
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const dayNames = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
|
||||
// Generate available years (10 years before and after current year)
|
||||
const availableYears = computed(() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
for (let y = currentYear - 10; y <= currentYear + 10; y++) {
|
||||
years.push(y);
|
||||
}
|
||||
return years;
|
||||
});
|
||||
|
||||
// Generate hours for 12-hour format
|
||||
const hours12 = computed(() => {
|
||||
const hours = [];
|
||||
for (let h = 1; h <= 12; h++) {
|
||||
hours.push({ value: h, label: h.toString() });
|
||||
}
|
||||
return hours;
|
||||
});
|
||||
|
||||
// Generate minutes
|
||||
const minutes = computed(() => {
|
||||
const mins = [];
|
||||
for (let m = 0; m < 60; m++) {
|
||||
mins.push(m);
|
||||
}
|
||||
return mins;
|
||||
});
|
||||
|
||||
// Generate calendar days for current month view
|
||||
const calendarDays = computed(() => {
|
||||
const days = [];
|
||||
const year = pickerYear.value;
|
||||
const month = pickerMonth.value;
|
||||
|
||||
// First day of the month
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const startingDay = firstDay.getDay();
|
||||
|
||||
// Last day of the month
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const totalDays = lastDay.getDate();
|
||||
|
||||
// Previous month days to fill the grid
|
||||
const prevMonthLastDay = new Date(year, month, 0).getDate();
|
||||
for (let i = startingDay - 1; i >= 0; i--) {
|
||||
days.push({
|
||||
day: prevMonthLastDay - i,
|
||||
date: new Date(year, month - 1, prevMonthLastDay - i),
|
||||
inMonth: false,
|
||||
isToday: false,
|
||||
isSelected: false
|
||||
});
|
||||
}
|
||||
|
||||
// Current month days
|
||||
const today = new Date();
|
||||
for (let d = 1; d <= totalDays; d++) {
|
||||
const date = new Date(year, month, d);
|
||||
const isToday = date.toDateString() === today.toDateString();
|
||||
const isSelected = pickerSelectedDate.value &&
|
||||
date.toDateString() === pickerSelectedDate.value.toDateString();
|
||||
days.push({
|
||||
day: d,
|
||||
date: date,
|
||||
inMonth: true,
|
||||
isToday: isToday,
|
||||
isSelected: isSelected
|
||||
});
|
||||
}
|
||||
|
||||
// Next month days to fill the grid (6 rows * 7 days = 42 total)
|
||||
const remainingDays = 42 - days.length;
|
||||
for (let d = 1; d <= remainingDays; d++) {
|
||||
days.push({
|
||||
day: d,
|
||||
date: new Date(year, month + 1, d),
|
||||
inMonth: false,
|
||||
isToday: false,
|
||||
isSelected: false
|
||||
});
|
||||
}
|
||||
|
||||
return days;
|
||||
});
|
||||
|
||||
const openDateTimePicker = (target, currentValue, callback) => {
|
||||
dateTimePickerTarget.value = target;
|
||||
dateTimePickerCallback.value = callback;
|
||||
|
||||
// Parse current value if exists
|
||||
if (currentValue) {
|
||||
const date = new Date(currentValue);
|
||||
if (!isNaN(date.getTime())) {
|
||||
pickerSelectedDate.value = date;
|
||||
pickerMonth.value = date.getMonth();
|
||||
pickerYear.value = date.getFullYear();
|
||||
|
||||
let hours = date.getHours();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12;
|
||||
hours = hours === 0 ? 12 : hours;
|
||||
|
||||
pickerHour.value = hours;
|
||||
pickerMinute.value = date.getMinutes();
|
||||
pickerAmPm.value = ampm;
|
||||
} else {
|
||||
setPickerToNow();
|
||||
}
|
||||
} else {
|
||||
setPickerToNow();
|
||||
}
|
||||
|
||||
showDateTimePicker.value = true;
|
||||
};
|
||||
|
||||
const setPickerToNow = () => {
|
||||
const now = new Date();
|
||||
pickerSelectedDate.value = now;
|
||||
pickerMonth.value = now.getMonth();
|
||||
pickerYear.value = now.getFullYear();
|
||||
|
||||
let hours = now.getHours();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12;
|
||||
hours = hours === 0 ? 12 : hours;
|
||||
|
||||
pickerHour.value = hours;
|
||||
pickerMinute.value = now.getMinutes();
|
||||
pickerAmPm.value = ampm;
|
||||
};
|
||||
|
||||
const closeDateTimePicker = () => {
|
||||
showDateTimePicker.value = false;
|
||||
dateTimePickerTarget.value = null;
|
||||
dateTimePickerCallback.value = null;
|
||||
};
|
||||
|
||||
const prevMonth = () => {
|
||||
if (pickerMonth.value === 0) {
|
||||
pickerMonth.value = 11;
|
||||
pickerYear.value--;
|
||||
} else {
|
||||
pickerMonth.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const nextMonth = () => {
|
||||
if (pickerMonth.value === 11) {
|
||||
pickerMonth.value = 0;
|
||||
pickerYear.value++;
|
||||
} else {
|
||||
pickerMonth.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePickerView = () => {
|
||||
// Called when month/year dropdowns change
|
||||
// The computed calendarDays will automatically update
|
||||
};
|
||||
|
||||
const selectDate = (date) => {
|
||||
pickerSelectedDate.value = date;
|
||||
};
|
||||
|
||||
const setToNow = () => {
|
||||
setPickerToNow();
|
||||
};
|
||||
|
||||
const setToToday = () => {
|
||||
const today = new Date();
|
||||
pickerSelectedDate.value = today;
|
||||
pickerMonth.value = today.getMonth();
|
||||
pickerYear.value = today.getFullYear();
|
||||
// Keep the current time
|
||||
};
|
||||
|
||||
const clearDateTime = () => {
|
||||
pickerSelectedDate.value = null;
|
||||
const now = new Date();
|
||||
pickerMonth.value = now.getMonth();
|
||||
pickerYear.value = now.getFullYear();
|
||||
pickerHour.value = 12;
|
||||
pickerMinute.value = 0;
|
||||
pickerAmPm.value = 'PM';
|
||||
};
|
||||
|
||||
const formatPickerPreview = () => {
|
||||
if (!pickerSelectedDate.value) return '';
|
||||
|
||||
const date = pickerSelectedDate.value;
|
||||
const monthName = monthNames[date.getMonth()];
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
|
||||
const hour = pickerHour.value;
|
||||
const minute = pickerMinute.value.toString().padStart(2, '0');
|
||||
const ampm = pickerAmPm.value;
|
||||
|
||||
return `${monthName} ${day}, ${year} at ${hour}:${minute} ${ampm}`;
|
||||
};
|
||||
|
||||
const applyDateTime = () => {
|
||||
if (!pickerSelectedDate.value) {
|
||||
// If no date selected, just close
|
||||
closeDateTimePicker();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the full datetime
|
||||
const date = new Date(pickerSelectedDate.value);
|
||||
let hours = pickerHour.value;
|
||||
|
||||
// Convert 12-hour to 24-hour
|
||||
if (pickerAmPm.value === 'AM') {
|
||||
hours = hours === 12 ? 0 : hours;
|
||||
} else {
|
||||
hours = hours === 12 ? 12 : hours + 12;
|
||||
}
|
||||
|
||||
date.setHours(hours);
|
||||
date.setMinutes(pickerMinute.value);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
|
||||
// Format as ISO string for storage (YYYY-MM-DDTHH:mm:ss)
|
||||
const isoString = date.toISOString().slice(0, 19);
|
||||
|
||||
// Call the callback with the result
|
||||
if (dateTimePickerCallback.value) {
|
||||
dateTimePickerCallback.value(isoString, date);
|
||||
}
|
||||
|
||||
closeDateTimePicker();
|
||||
};
|
||||
|
||||
// Helper to open datetime picker for meeting date
|
||||
const openMeetingDatePicker = () => {
|
||||
if (!selectedRecording.value) return;
|
||||
|
||||
openDateTimePicker(
|
||||
'meeting_date',
|
||||
selectedRecording.value.meeting_date,
|
||||
(isoString) => {
|
||||
selectedRecording.value.meeting_date = isoString;
|
||||
// Auto-save the change
|
||||
saveInlineMeetingDate();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Save meeting date inline (similar to other inline edits)
|
||||
const saveInlineMeetingDate = async () => {
|
||||
if (!selectedRecording.value) return;
|
||||
|
||||
const fullPayload = {
|
||||
id: selectedRecording.value.id,
|
||||
title: selectedRecording.value.title,
|
||||
participants: selectedRecording.value.participants,
|
||||
notes: selectedRecording.value.notes,
|
||||
summary: selectedRecording.value.summary,
|
||||
meeting_date: selectedRecording.value.meeting_date
|
||||
};
|
||||
|
||||
try {
|
||||
const csrfTokenValue = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch('/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfTokenValue
|
||||
},
|
||||
body: JSON.stringify(fullPayload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to save meeting date');
|
||||
|
||||
showToast('Meeting date updated!', 'fa-calendar-check');
|
||||
} catch (error) {
|
||||
showToast(`Failed to save: ${error.message}`, 'fa-exclamation-circle', 3000, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// Edit modal
|
||||
openEditModal,
|
||||
editRecording,
|
||||
cancelEdit,
|
||||
saveEdit,
|
||||
|
||||
// Delete modal
|
||||
confirmDelete,
|
||||
cancelDelete,
|
||||
deleteRecording,
|
||||
|
||||
// Archive
|
||||
archiveRecording,
|
||||
|
||||
// Tags modal
|
||||
openEditTagsModal,
|
||||
editRecordingTags,
|
||||
closeEditTagsModal,
|
||||
addTagToRecording,
|
||||
removeTagFromRecording,
|
||||
|
||||
// Reset modal
|
||||
openResetModal,
|
||||
cancelReset,
|
||||
resetRecording,
|
||||
|
||||
// System audio help
|
||||
openSystemAudioHelpModal,
|
||||
closeSystemAudioHelpModal,
|
||||
|
||||
// Toast
|
||||
dismissToast,
|
||||
|
||||
// DateTime picker
|
||||
monthNames,
|
||||
dayNames,
|
||||
availableYears,
|
||||
hours12,
|
||||
minutes,
|
||||
calendarDays,
|
||||
openDateTimePicker,
|
||||
closeDateTimePicker,
|
||||
prevMonth,
|
||||
nextMonth,
|
||||
updatePickerView,
|
||||
selectDate,
|
||||
setToNow,
|
||||
setToToday,
|
||||
clearDateTime,
|
||||
formatPickerPreview,
|
||||
applyDateTime,
|
||||
openMeetingDatePicker
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user