import { ref, computed, onMounted, onUnmounted } from 'vue' import { defineStore } from 'pinia' import { callAdminForthApi, handleNotAuthorized } from '@/utils'; import { isCompositePrimaryKey, recordIdFilters } from '@/utils/recordId'; import websocket from '@/websocket'; import { useAdminforth } from '@/adminforth'; import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, GetConfigResponse, ResourceVeryShort, AdminUser, UserData, AdminForthConfigMenuItem, AdminForthConfigForFrontend, AdminForthResourceFrontend } from '@/types/Common'; import type { Ref } from 'vue' export const useCoreStore = defineStore('core', () => { const { alert } = useAdminforth(); const resourceById: Ref> = ref({}); const theme: Ref<'light'| 'dark'> = ref(window.localStorage.getItem('af__theme') as ('light'|'dark') || 'light'); const menu: Ref = ref([]); const config: Ref = ref(null); const record: Ref = ref({}); const resource: Ref = ref(null); const userData: Ref = ref(null); const isResourceFetching = ref(false); const isInternetError = ref(false); const screenWidth = ref(window.innerWidth); const listRecordIds: Ref = ref([]); const listResourceId: Ref = ref(null); const listFilters: Ref = ref([]); const listSort: Ref = ref([]); const listPage: Ref = ref(0); const listPageSize: Ref = ref(0); onMounted(() => { window.addEventListener('resize', updateWidth); }); onUnmounted(() => { window.removeEventListener('resize', updateWidth); }); const isMobile = computed(() => screenWidth.value <= 768); const updateWidth = () => { screenWidth.value = window.innerWidth } const resourceColumnsWithFilters = computed(() => { if (!resource.value) { return []; } return resource.value.columns.filter((col: AdminForthResourceColumnCommon) => col.showIn?.filter); }) const resourceOptions: Ref = ref(null); const resourceColumnsError: Ref = ref(''); const resourceColumnsId: Ref = ref(null); const adminUser: Ref = ref(null); async function resetAdminUser() { adminUser.value = null; } async function resetResource() { resource.value = null; } async function toggleTheme() { theme.value = theme.value === 'light' ? 'dark' : 'light'; if (theme.value === 'light') { document.documentElement.classList.remove('dark'); } else { document.documentElement.classList.add('dark'); } document.documentElement.setAttribute('data-theme', theme.value); theme.value = theme.value; window.localStorage.setItem('af__theme', theme.value); } async function fetchConfig( { redirectToLoginIfNotLoggedIn = true }: { redirectToLoginIfNotLoggedIn?: boolean } = {} ): Promise { const resp: GetConfigResponse | null = await callAdminForthApi({ path: '/get_config', method: 'GET', }); if(!resp){ return null } config.value = { ...config.value, ...resp.config } as AdminForthConfigForFrontend; if (!resp.loggedIn) { if (redirectToLoginIfNotLoggedIn) { await handleNotAuthorized(); } return resp; } menu.value = resp.menu; resourceById.value = resp.resources.reduce((acc: Record, resource: ResourceVeryShort) => { acc[resource.resourceId] = resource; return acc; }, {}); adminUser.value = resp.adminUser; userData.value = resp.user; console.log('🌍 AdminForth v', resp.version); subscribeToMenuRefresh(); return resp; } async function fetchMenuAndResource() { await fetchConfig(); } /** * @deprecated kept for plugins which were written when public config had own endpoint. Use fetchConfig instead. * Old endpoint was noAuth and never redirected to login, so anonymous answer is not redirected here as well. */ async function getPublicConfig() { await fetchConfig({ redirectToLoginIfNotLoggedIn: false }); } async function refreshMenu() { await fetchMenuAndResource(); await fetchMenuBadges(); } function subscribeToMenuRefresh() { if (!userData.value?.pk) { return; } websocket.unsubscribeByPrefix('/opentopic/refresh-menu/'); websocket.subscribe(`/opentopic/refresh-menu/${userData.value.pk}`, refreshMenu); } function findItemWithId(items: AdminForthConfigMenuItem[], itemId: string): AdminForthConfigMenuItem | undefined { for (const item of items) { if (item.itemId === itemId) { return item; } if (item.children) { const found = findItemWithId(item.children, itemId); if (found) { return found; } } } } async function subscribeToMenuBadges() { const processItem = (mi: AdminForthConfigMenuItem) => { // console.log('🔔 subscribeToMenuBadges', mi.badge, JSON.stringify(mi)); if (mi.badge !== undefined) { websocket.unsubscribe(`/opentopic/update-menu-badge/${mi.itemId}`); websocket.subscribe(`/opentopic/update-menu-badge/${mi.itemId}`, ({ badge }) => { mi.badge = badge; }); } } menu.value.forEach((mi) => { processItem(mi); if (mi.children) { mi.children.forEach((child) => { processItem(child); }) } }) } async function fetchMenuBadges() { const resp: Record = await callAdminForthApi({ path: '/get_menu_badges', method: 'GET', }); if (!resp) { return; } Object.entries(resp).forEach(([itemId, badge]: [string, string]) => { const item: AdminForthConfigMenuItem | undefined = findItemWithId(menu.value, itemId); if (item) { item.badge = badge; } }); subscribeToMenuBadges(); } async function fetchRecord({ resourceId, primaryKey, source }: { resourceId: string, primaryKey: string, source: string }) { record.value = null; if (!resource.value) { throw new Error('Columns not fetched yet'); } let filters; if (isCompositePrimaryKey(resource.value)) { filters = recordIdFilters(resource.value, primaryKey); } else { const col = resource.value.columns.find((col: AdminForthResourceColumnCommon) => col.primaryKey); if (!col) { throw new Error(`Primary key not found in resource ${resourceId}`); } filters = [ { field: col.name, operator: 'eq', value: primaryKey } ]; } const respData = await callAdminForthApi({ path: '/get_resource_data', method: 'POST', body: { source: source, resourceId: resourceId, filters, sort: [], limit: 1, offset: 0 } }); if (respData.error) { alert({ message: respData.error, variant: 'danger', timeout: 30, }); record.value = {}; } else { record.value = respData.data[0]; } } async function fetchResourceFull({ resourceId, forceFetch }: { resourceId: string, forceFetch?: boolean }) { if (resourceColumnsId.value === resourceId && resource.value && !forceFetch) { // already fetched return; } isResourceFetching.value = true; resourceColumnsId.value = resourceId; resourceColumnsError.value = ''; const res = await callAdminForthApi({ path: '/get_resource', method: 'POST', body: { resourceId, } }); if (!res) { isResourceFetching.value = false; return; } if (res.error) { resourceColumnsError.value = res.error; } else { resourceById.value[resourceId] = res.resource; resource.value = res.resource; resourceOptions.value = res.resource.options; } isResourceFetching.value = false; } async function getLoginFormConfig() { const res = await callAdminForthApi({ path: '/get_login_form_config', method: 'GET', }); console.log('📦 getLoginFormConfig', res); config.value = {...config.value, ...res}; } const username = computed(() => { const usernameField = config.value?.usernameField; return userData.value && usernameField && userData.value[usernameField]; }); const userFullname = computed(() => { const userFullnameField = config.value?.userFullnameField; return userData.value && userFullnameField && userData.value[userFullnameField]; }) const userAvatarUrl = computed(() => { return userData.value?.userAvatarUrl || null; }); const isIos = computed(() => { return ( /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document) )}); return { config, resourceById, menu, username, userFullname, userAvatarUrl, fetchConfig, getPublicConfig, fetchMenuAndResource, refreshMenu, getLoginFormConfig, fetchRecord, record, fetchResourceFull, resourceColumnsError, resourceOptions, resource, adminUser, resourceColumnsWithFilters, toggleTheme, theme, fetchMenuBadges, resetAdminUser, resetResource, isResourceFetching, isIos, isInternetError, isMobile, listRecordIds, listResourceId, listFilters, listSort, listPage, listPageSize, } })