686 lines
22 KiB
TypeScript
686 lines
22 KiB
TypeScript
import { ref, computed, shallowRef } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
|
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
|
import { useMailSync } from '@MailManager/composables/useMailSync'
|
|
import { useSnackbar } from '@KTXC'
|
|
import type { ServiceIdentifier, CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
|
import type { EntityTransmitRequest } from '@MailManager/types/entity'
|
|
import type { MessageAddressInterface, MessageInterface, MessagePartInterface } from '@MailManager/types/message'
|
|
import { ServiceObject, type CollectionObject, type EntityObject } from '@MailManager/models'
|
|
import { CollectionPropertiesObject } from '@MailManager/models/collection'
|
|
|
|
interface ComposerMessageInput {
|
|
to: string[]
|
|
cc: string[]
|
|
bcc: string[]
|
|
subject: string
|
|
body: {
|
|
html: string
|
|
text: string
|
|
}
|
|
}
|
|
|
|
export const useMailStore = defineStore('mailStore', () => {
|
|
const servicesStore = useServicesStore()
|
|
const collectionsStore = useCollectionsStore()
|
|
const entitiesStore = useEntitiesStore()
|
|
const { showSnackbar } = useSnackbar()
|
|
|
|
// Background mail sync
|
|
const mailSyncController = useMailSync({
|
|
interval: 30000,
|
|
autoStart: false,
|
|
fetchDetails: true,
|
|
})
|
|
const mailSync = {
|
|
isRunning: mailSyncController.isRunning,
|
|
lastSync: mailSyncController.lastSync,
|
|
error: mailSyncController.error,
|
|
sync: mailSyncController.sync,
|
|
start: mailSyncController.start,
|
|
stop: mailSyncController.stop,
|
|
restart: mailSyncController.restart,
|
|
}
|
|
|
|
// ── General State ─────────────────-───────────────────────────────────────
|
|
const loading = ref(false)
|
|
const serviceFolderLoadingState = ref<Record<string, boolean>>({})
|
|
const serviceFolderLoadedState = ref<Record<string, boolean>>({})
|
|
const serviceFolderErrorState = ref<Record<string, string | null>>({})
|
|
|
|
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
|
const selectedMessage = shallowRef<EntityObject | null>(null)
|
|
const composerSaving = ref(false)
|
|
const composerSending = ref(false)
|
|
const composerLastSaved = ref<Date | null>(null)
|
|
const composerDraftIdentifier = ref<EntityIdentifier | null>(null)
|
|
|
|
// ── Computed ──────────────────────────────────────────────────────────────
|
|
const currentMessages = computed(() => {
|
|
if (!selectedFolder.value) return []
|
|
|
|
const folder = selectedFolder.value
|
|
|
|
return entitiesStore.entities.filter(e =>
|
|
e.provider === folder.provider &&
|
|
String(e.service) === String(folder.service) &&
|
|
String(e.collection) === String(folder.identifier),
|
|
)
|
|
})
|
|
|
|
// ── Initialization ────────────────────────────────────────────────────────
|
|
|
|
async function initialize() {
|
|
loading.value = true
|
|
try {
|
|
await servicesStore.list()
|
|
|
|
const services = [...servicesStore.servicesEnabled]
|
|
await Promise.all(services.map(service => loadFoldersForService(service)))
|
|
} catch (error) {
|
|
console.error('[Mail][Operations] Failed to initialize:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadFoldersForService(service: ServiceObject) {
|
|
|
|
if (service.identifier === null) {
|
|
return
|
|
}
|
|
|
|
_setServiceFolderLoading(service.provider, service.identifier, true)
|
|
_setServiceFolderError(service.provider, service.identifier, null)
|
|
|
|
try {
|
|
// retrieve folders for service
|
|
await collectionsStore.collectionsForService(service.provider, service.identifier, true)
|
|
|
|
_setServiceFolderLoaded(service.provider, service.identifier, true)
|
|
|
|
_updateSyncSources()
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Failed to load folders'
|
|
_setServiceFolderError(service.provider, service.identifier, message)
|
|
console.error(`[Mail][Operations] Failed to load folders for ${service.provider}:${String(service.identifier)}:`, error)
|
|
_updateSyncSources()
|
|
return {}
|
|
} finally {
|
|
_setServiceFolderLoading(service.provider, service.identifier, false)
|
|
}
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
function _serviceIdentifier(item: ServiceObject | CollectionObject | EntityObject | { provider: string, service: string | number }): ServiceIdentifier {
|
|
if (item instanceof ServiceObject) {
|
|
return `${item.provider}:${String(item.identifier)}` as ServiceIdentifier
|
|
}
|
|
return `${item.provider}:${String(item.service)}` as ServiceIdentifier
|
|
}
|
|
|
|
function _sameCollection(left: CollectionObject | null | undefined, right: CollectionObject | null | undefined): boolean {
|
|
if (!left || !right) {
|
|
return false
|
|
}
|
|
|
|
return left.provider === right.provider &&
|
|
String(left.service) === String(right.service) &&
|
|
String(left.identifier) === String(right.identifier)
|
|
}
|
|
|
|
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
|
|
serviceFolderLoadingState.value = {
|
|
...serviceFolderLoadingState.value,
|
|
[_serviceIdentifier({ provider, service })]: loadingState,
|
|
}
|
|
}
|
|
|
|
function _setServiceFolderLoaded(provider: string, service: string | number, loaded: boolean) {
|
|
serviceFolderLoadedState.value = {
|
|
...serviceFolderLoadedState.value,
|
|
[_serviceIdentifier({ provider, service })]: loaded,
|
|
}
|
|
}
|
|
|
|
function _setServiceFolderError(provider: string, service: string | number, error: string | null) {
|
|
serviceFolderErrorState.value = {
|
|
...serviceFolderErrorState.value,
|
|
[_serviceIdentifier({ provider, service })]: error,
|
|
}
|
|
}
|
|
|
|
function _updateSyncSources() {
|
|
mailSyncController.clearSources()
|
|
|
|
// Track the currently selected folder
|
|
if (selectedFolder.value) {
|
|
//mailSyncController.addSource({
|
|
// provider: selectedFolder.value.provider,
|
|
// service: selectedFolder.value.service,
|
|
// collections: [selectedFolder.value.identifier],
|
|
//})
|
|
}
|
|
|
|
// Always track inboxes for each account (for new-mail notifications)
|
|
servicesStore.servicesEnabled.forEach(service => {
|
|
if (service.identifier === null) {
|
|
return
|
|
}
|
|
const inboxes = collectionsStore.collectionsForService(service.provider, service.identifier).filter(
|
|
c =>
|
|
String(c.service) === String(service.identifier) &&
|
|
(c.properties.role === 'inbox' ||
|
|
String(c.identifier).toLowerCase() === 'inbox'),
|
|
)
|
|
|
|
if (inboxes.length > 0) {
|
|
//mailSyncController.addSource({
|
|
// provider: service.provider,
|
|
// service: service.identifier as string | number,
|
|
// collections: inboxes.map(inbox => inbox.identifier),
|
|
//})
|
|
}
|
|
})
|
|
|
|
if (mailSyncController.sources.value.length > 0 && !mailSyncController.isRunning.value) {
|
|
mailSyncController.start()
|
|
}
|
|
}
|
|
|
|
function isServiceFolderLoading(provider: string, service: string | number) {
|
|
return serviceFolderLoadingState.value[_serviceIdentifier({ provider, service })] === true
|
|
}
|
|
|
|
function hasServiceFoldersLoaded(provider: string, service: string | number) {
|
|
return serviceFolderLoadedState.value[_serviceIdentifier({ provider, service })] === true
|
|
}
|
|
|
|
function getServiceFolderError(provider: string, service: string | number) {
|
|
return serviceFolderErrorState.value[_serviceIdentifier({ provider, service })] ?? null
|
|
}
|
|
|
|
function _findDraftFolder(folder: CollectionObject): CollectionObject {
|
|
return collectionsStore.collectionsForService(folder.provider, folder.service).find(
|
|
candidate =>
|
|
candidate.provider === folder.provider &&
|
|
String(candidate.service) === String(folder.service) &&
|
|
(candidate.properties.role === 'drafts' ||
|
|
String(candidate.identifier).toLowerCase() === 'drafts' ||
|
|
candidate.properties.label.toLowerCase() === 'drafts'),
|
|
) ?? folder
|
|
}
|
|
|
|
function _toMessageAddresses(addresses: string[]): MessageAddressInterface[] | undefined {
|
|
const normalized = addresses
|
|
.map(address => address.trim())
|
|
.filter(address => address.length > 0)
|
|
|
|
if (normalized.length === 0) {
|
|
return undefined
|
|
}
|
|
|
|
return normalized.map(address => ({ address }))
|
|
}
|
|
|
|
function _toDraftBody(body: ComposerMessageInput['body']): MessagePartInterface | null {
|
|
const parts: MessagePartInterface[] = []
|
|
const text = body.text.trim()
|
|
const html = body.html.trim()
|
|
|
|
if (text.length > 0) {
|
|
parts.push({
|
|
type: 'text/plain',
|
|
content: text,
|
|
})
|
|
}
|
|
|
|
if (html.length > 0) {
|
|
parts.push({
|
|
type: 'text/html',
|
|
content: html,
|
|
})
|
|
}
|
|
|
|
if (parts.length === 0) {
|
|
return null
|
|
}
|
|
|
|
if (parts.length === 1) {
|
|
return parts[0]
|
|
}
|
|
|
|
return {
|
|
type: 'multipart/alternative',
|
|
subParts: parts,
|
|
}
|
|
}
|
|
|
|
function _toDraftProperties(message: ComposerMessageInput): MessageInterface {
|
|
return {
|
|
'@type': 'mail:message',
|
|
to: _toMessageAddresses(message.to),
|
|
cc: _toMessageAddresses(message.cc),
|
|
bcc: _toMessageAddresses(message.bcc),
|
|
subject: message.subject.trim() || null,
|
|
body: _toDraftBody(message.body),
|
|
flags: {
|
|
draft: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
function resetComposerState() {
|
|
composerSaving.value = false
|
|
composerSending.value = false
|
|
composerLastSaved.value = null
|
|
composerDraftIdentifier.value = null
|
|
}
|
|
|
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
|
|
async function retrieveService(identifier: ServiceIdentifier, force: boolean = false): Promise<ServiceObject | null> {
|
|
let service = servicesStore.serviceByIdentifier(identifier)
|
|
if (service && !force) {
|
|
return service
|
|
}
|
|
|
|
try {
|
|
service = await servicesStore.serviceByIdentifier(identifier, true)
|
|
} catch (error) {
|
|
console.error(`[Mail][Operations] Failed to retrieve service ${identifier}:`, error)
|
|
throw error
|
|
}
|
|
|
|
if (!service) {
|
|
const message = `Service ${identifier} not found`
|
|
console.error(`[Mail][Operations] ${message}`)
|
|
throw new Error(message)
|
|
}
|
|
|
|
return service
|
|
}
|
|
|
|
async function selectFolder(folder: CollectionObject | null) {
|
|
selectedFolder.value = folder
|
|
selectedMessage.value = null
|
|
|
|
if (folder) {
|
|
try {
|
|
await entitiesStore.list([folder.identifier])
|
|
} catch (error) {
|
|
console.error('[Mail][Operations] Failed to load messages:', error)
|
|
}
|
|
}
|
|
|
|
_updateSyncSources()
|
|
}
|
|
|
|
function selectMessage(entity: EntityObject | null) {
|
|
selectedMessage.value = entity
|
|
}
|
|
|
|
async function saveComposerDraft(folder: CollectionObject, message: ComposerMessageInput) {
|
|
composerSaving.value = true
|
|
|
|
try {
|
|
const targetFolder = _findDraftFolder(folder)
|
|
const properties = _toDraftProperties(message)
|
|
const draft = composerDraftIdentifier.value
|
|
? await entitiesStore.update(composerDraftIdentifier.value, properties)
|
|
: await entitiesStore.create(targetFolder.identifier, properties)
|
|
|
|
composerDraftIdentifier.value = draft.identifier
|
|
composerLastSaved.value = new Date()
|
|
|
|
return draft
|
|
} catch (error) {
|
|
console.error('[Mail][Operations] Failed to save draft:', error)
|
|
throw error
|
|
} finally {
|
|
composerSaving.value = false
|
|
}
|
|
}
|
|
|
|
function findFoldersByRole(role: string): CollectionObject[] {
|
|
const normalizedRole = role.toLowerCase()
|
|
|
|
return servicesStore.servicesEnabled.flatMap(service => {
|
|
if (service.identifier === null) {
|
|
return []
|
|
}
|
|
|
|
return collectionsStore.collectionsForService(service.provider, service.identifier).filter(
|
|
folder =>
|
|
folder.provider === service.provider &&
|
|
String(folder.service) === String(service.identifier) &&
|
|
(folder.properties.role === normalizedRole ||
|
|
String(folder.identifier).toLowerCase() === normalizedRole),
|
|
)
|
|
})
|
|
}
|
|
|
|
async function sendComposerMessage(message: ComposerMessageInput) {
|
|
composerSending.value = true
|
|
|
|
const transmitRequest: EntityTransmitRequest = {
|
|
message: {
|
|
to: message.to.map(address => address.trim()).filter(address => address.length > 0),
|
|
cc: message.cc.map(address => address.trim()).filter(address => address.length > 0),
|
|
bcc: message.bcc.map(address => address.trim()).filter(address => address.length > 0),
|
|
subject: message.subject,
|
|
body: {
|
|
html: message.body.html,
|
|
text: message.body.text,
|
|
},
|
|
},
|
|
}
|
|
|
|
if (transmitRequest.message.cc?.length === 0) {
|
|
delete transmitRequest.message.cc
|
|
}
|
|
|
|
if (transmitRequest.message.bcc?.length === 0) {
|
|
delete transmitRequest.message.bcc
|
|
}
|
|
|
|
try {
|
|
const response = await entitiesStore.transmit(transmitRequest)
|
|
|
|
if (composerDraftIdentifier.value) {
|
|
try {
|
|
await entitiesStore.delete([composerDraftIdentifier.value])
|
|
} catch (error) {
|
|
console.error('[Mail][Operations] Failed to delete draft after send:', error)
|
|
}
|
|
}
|
|
|
|
notify('Message sent', 'success')
|
|
resetComposerState()
|
|
return response
|
|
} catch (error) {
|
|
const messageText = error instanceof Error ? error.message : 'Failed to send message'
|
|
console.error('[Mail][Operations] Failed to send message:', error)
|
|
notify(messageText, 'error')
|
|
throw error
|
|
} finally {
|
|
composerSending.value = false
|
|
}
|
|
}
|
|
|
|
async function createFolder(service: ServiceObject, label: string, parentFolder: CollectionObject | null = null): Promise<CollectionObject> {
|
|
if (service.identifier === null) {
|
|
throw new Error('Cannot create folder for a service without an identifier')
|
|
}
|
|
|
|
const properties = new CollectionPropertiesObject()
|
|
properties.label = label.trim()
|
|
properties.rank = 0
|
|
properties.subscribed = true
|
|
|
|
const newFolder = await collectionsStore.create(
|
|
service.provider,
|
|
service.identifier,
|
|
properties,
|
|
parentFolder?.identifier,
|
|
)
|
|
|
|
notify(
|
|
`Folder "${newFolder.properties.label || properties.label}" created`,
|
|
'success',
|
|
)
|
|
|
|
return newFolder
|
|
}
|
|
|
|
async function renameFolder(folder: CollectionObject, label: string): Promise<CollectionObject> {
|
|
const properties = new CollectionPropertiesObject()
|
|
properties.label = label.trim()
|
|
properties.rank = folder.properties.rank ?? 0
|
|
properties.subscribed = folder.properties.subscribed ?? true
|
|
|
|
const updatedFolder = await collectionsStore.update(folder.identifier, properties)
|
|
|
|
if (_sameCollection(selectedFolder.value, folder)) {
|
|
selectedFolder.value = updatedFolder
|
|
}
|
|
|
|
notify(
|
|
`Folder "${folder.properties.label || String(folder.identifier)}" renamed to "${updatedFolder.properties.label || properties.label}"`,
|
|
'success',
|
|
)
|
|
|
|
return updatedFolder
|
|
}
|
|
|
|
async function moveFolder(source: CollectionObject, target: CollectionObject): Promise<CollectionObject> {
|
|
const movedFolder = await collectionsStore.move(target.identifier, source.identifier)
|
|
|
|
if (_sameCollection(selectedFolder.value, source)) {
|
|
selectedFolder.value = movedFolder
|
|
}
|
|
|
|
notify(
|
|
`Folder "${source.properties.label || String(source.identifier)}" moved to "${target.properties.label || String(target.identifier)}"`,
|
|
'success',
|
|
)
|
|
|
|
return movedFolder
|
|
}
|
|
|
|
async function deleteFolder(folder: CollectionObject): Promise<CollectionObject | boolean> {
|
|
const deletedFolder = await collectionsStore.delete(folder.identifier)
|
|
|
|
if (_sameCollection(selectedFolder.value, folder)) {
|
|
await selectFolder(null)
|
|
}
|
|
|
|
notify(
|
|
`Folder "${folder.properties.label || String(folder.identifier)}" deleted`,
|
|
'success',
|
|
)
|
|
|
|
return deletedFolder
|
|
}
|
|
|
|
async function deleteMessages(entityIdentifiers: EntityIdentifier[]) {
|
|
if (entityIdentifiers.length === 0) {
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
|
|
try {
|
|
const { successes, failures } = await entitiesStore.delete(entityIdentifiers)
|
|
|
|
if (failures.length === 0) {
|
|
notify(
|
|
successes.length === 1 ? 'Message deleted' : `${successes.length} messages deleted`,
|
|
'success',
|
|
)
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
notify(
|
|
successes.length === 0
|
|
? `Delete failed for ${failures.length === 1 ? '1 message' : `${failures.length} messages`}`
|
|
: `Deleted ${successes.length} ${successes.length === 1 ? 'message' : 'messages'}. ${failures.length} failed.`,
|
|
successes.length === 0 ? 'error' : 'warning',
|
|
)
|
|
}
|
|
} catch (error) {
|
|
const messageText = error instanceof Error ? error.message : 'Failed to delete messages'
|
|
console.error('[Mail][Operations] Failed to delete messages:', error)
|
|
notify(messageText, 'error')
|
|
throw error
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function flagMessages(entityIdentifiers: EntityIdentifier[], flags: Partial<MessageInterface['flags']>, options: { notify?: boolean } = {}) {
|
|
if (entityIdentifiers.length === 0) {
|
|
return
|
|
}
|
|
|
|
const shouldNotify = options.notify ?? true
|
|
|
|
loading.value = true
|
|
|
|
try {
|
|
const patch = entitiesStore.fresh().properties
|
|
patch.flags = flags
|
|
|
|
const { successes, failures } = await entitiesStore.patch(patch, entityIdentifiers)
|
|
|
|
if (shouldNotify && successes.length > 0) {
|
|
notify(
|
|
successes.length === 1 ? 'Message updated' : `${successes.length} messages updated`,
|
|
'success',
|
|
)
|
|
}
|
|
|
|
if (shouldNotify && failures.length > 0) {
|
|
notify(
|
|
successes.length === 0
|
|
? `Update failed for ${failures.length === 1 ? '1 message' : `${failures.length} messages`}`
|
|
: `Updated ${successes.length} ${successes.length === 1 ? 'message' : 'messages'}. ${failures.length} failed.`,
|
|
successes.length === 0 ? 'error' : 'warning',
|
|
)
|
|
}
|
|
} catch (error) {
|
|
const messageText = error instanceof Error ? error.message : 'Failed to update messages'
|
|
console.error('[Mail][Operations] Failed to update messages:', error)
|
|
notify(messageText, 'error')
|
|
throw error
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function moveMessages(target: CollectionObject, entityIdentifiers: EntityIdentifier[]) {
|
|
const { movableIdentifiers, sourceCollections } = entityIdentifiers.reduce(
|
|
(accumulator, identifier) => {
|
|
const entity = entitiesStore.entity(identifier)
|
|
|
|
if (!entity) {
|
|
return accumulator
|
|
}
|
|
|
|
// Only allow moving messages within the same service and disallow moving into the same folder
|
|
const canMove = entity.provider === target.provider &&
|
|
String(entity.service) === String(target.service) &&
|
|
String(entity.collection) !== String(target.identifier)
|
|
|
|
if (!canMove) {
|
|
return accumulator
|
|
}
|
|
|
|
accumulator.movableIdentifiers.push(identifier)
|
|
|
|
if (!accumulator.sourceCollections.some(
|
|
collection => String(collection) === String(entity.collection),
|
|
)) {
|
|
accumulator.sourceCollections.push(entity.collection)
|
|
}
|
|
|
|
return accumulator
|
|
},
|
|
{
|
|
movableIdentifiers: [] as EntityIdentifier[],
|
|
sourceCollections: [] as CollectionIdentifier[],
|
|
},
|
|
)
|
|
|
|
if (movableIdentifiers.length === 0) {
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
|
|
try {
|
|
const { successes, failures } = await entitiesStore.move(target.identifier, movableIdentifiers)
|
|
|
|
if (failures.length === 0) {
|
|
notify(
|
|
successes.length === 1 ? 'Message moved' : `${successes.length} messages moved`,
|
|
'success',
|
|
)
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
notify(
|
|
successes.length === 0
|
|
? `Move failed for ${failures.length === 1 ? '1 message' : `${failures.length} messages`}`
|
|
: `Moved ${successes.length} ${successes.length === 1 ? 'message' : 'messages'}. ${failures.length} failed.`,
|
|
successes.length === 0 ? 'error' : 'warning',
|
|
)
|
|
}
|
|
|
|
// update source collections to reflect moved messages
|
|
sourceCollections.push(target.identifier)
|
|
await collectionsStore.fetch(sourceCollections)
|
|
} catch (error) {
|
|
const messageText = error instanceof Error ? error.message : 'Failed to move messages'
|
|
console.error('[Mail][Operations] Failed to move messages:', error)
|
|
notify(messageText, 'error')
|
|
throw error
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function notify(message: string, color: 'success' | 'error' | 'info' | 'warning' = 'success') {
|
|
showSnackbar({ message, color })
|
|
}
|
|
|
|
// ── Exports ───────────────────────────────────────────────────────────────
|
|
|
|
return {
|
|
// Sub-stores (forwarded for template convenience)
|
|
collectionsStore,
|
|
entitiesStore,
|
|
servicesStore,
|
|
mailSync,
|
|
|
|
// State
|
|
loading,
|
|
selectedMessage,
|
|
composerSaving,
|
|
composerSending,
|
|
composerLastSaved,
|
|
composerDraftIdentifier,
|
|
serviceFolderLoadingState,
|
|
serviceFolderLoadedState,
|
|
serviceFolderErrorState,
|
|
|
|
// Computed
|
|
currentMessages,
|
|
|
|
// Actions
|
|
retrieveService,
|
|
selectFolder,
|
|
selectMessage,
|
|
createFolder,
|
|
saveComposerDraft,
|
|
sendComposerMessage,
|
|
resetComposerState,
|
|
flagMessages,
|
|
deleteMessages,
|
|
deleteFolder,
|
|
moveMessages,
|
|
moveFolder,
|
|
renameFolder,
|
|
notify,
|
|
isServiceFolderLoading,
|
|
hasServiceFoldersLoaded,
|
|
getServiceFolderError,
|
|
findFoldersByRole,
|
|
loadFoldersForService,
|
|
initialize,
|
|
}
|
|
})
|