Compare commits
3 Commits
6e8ad1c412
...
2d881cb152
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d881cb152 | |||
| 835c287810 | |||
| 8ab44fc9da |
@ -21,7 +21,12 @@ const DocumentPrintButton = ({
|
||||
disabled = false,
|
||||
...buttonProps
|
||||
}) => {
|
||||
const { fetchObjects } = useContext(ApiServerContext)
|
||||
const {
|
||||
fetchObjects,
|
||||
connected,
|
||||
subscribeToObjectUpdates,
|
||||
subscribeToObjectTypeUpdates
|
||||
} = useContext(ApiServerContext)
|
||||
const fetchObjectsRef = useRef(fetchObjects)
|
||||
fetchObjectsRef.current = fetchObjects
|
||||
|
||||
@ -30,34 +35,59 @@ const DocumentPrintButton = ({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [newDocumentJobOpen, setNewDocumentJobOpen] = useState(false)
|
||||
|
||||
const subscribedIdsRef = useRef([])
|
||||
const unsubscribesRef = useRef([])
|
||||
const subscribeToObjectTypeUpdatesRef = useRef(null)
|
||||
const updateEventHandlerRef = useRef()
|
||||
|
||||
const { token } = useContext(AuthContext)
|
||||
|
||||
// Get the model by name
|
||||
//const model = getModelByName(type)
|
||||
const subscriptionFilter = useMemo(
|
||||
() => ({
|
||||
objectType: type,
|
||||
global: false,
|
||||
active: true
|
||||
}),
|
||||
[type]
|
||||
)
|
||||
|
||||
const loadDocumentTemplates = useCallback(async () => {
|
||||
if (!type || token == null) return
|
||||
const templateMatchesFilter = useCallback(
|
||||
(template) =>
|
||||
template?.objectType === type &&
|
||||
template?.global === false &&
|
||||
template?.active === true,
|
||||
[type]
|
||||
)
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await fetchObjectsRef.current('documentTemplate', {
|
||||
filter: {
|
||||
objectType: type,
|
||||
global: false,
|
||||
active: true
|
||||
},
|
||||
limit: 100 // Get more templates to show in dropdown
|
||||
})
|
||||
const loadDocumentTemplates = useCallback(
|
||||
async (silent = false) => {
|
||||
if (!type || token == null) return
|
||||
|
||||
if (result && result.data) {
|
||||
setDocumentTemplates(result.data)
|
||||
if (!silent) {
|
||||
setLoading(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching document templates:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [type, token])
|
||||
try {
|
||||
const result = await fetchObjectsRef.current('documentTemplate', {
|
||||
filter: subscriptionFilter,
|
||||
limit: 100 // Get more templates to show in dropdown
|
||||
})
|
||||
|
||||
if (result && result.data) {
|
||||
setDocumentTemplates(result.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching document templates:', error)
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[type, token, subscriptionFilter]
|
||||
)
|
||||
|
||||
const loadDocumentTemplatesRef = useRef(loadDocumentTemplates)
|
||||
loadDocumentTemplatesRef.current = loadDocumentTemplates
|
||||
|
||||
// Stable key from objectData._id (excludes _isEditing so toggling edit mode doesn't trigger template reload)
|
||||
const objectKey = useMemo(() => {
|
||||
@ -70,6 +100,103 @@ const DocumentPrintButton = ({
|
||||
loadDocumentTemplates()
|
||||
}, [objectKey, token, type, loadDocumentTemplates])
|
||||
|
||||
const updateEventHandler = useCallback(
|
||||
(id, updatedData) => {
|
||||
setDocumentTemplates((prev) => {
|
||||
const existing = prev.find((template) => template._id === id)
|
||||
const merged = existing
|
||||
? { ...existing, ...updatedData }
|
||||
: { _id: id, ...updatedData }
|
||||
|
||||
if (!templateMatchesFilter(merged)) {
|
||||
return prev.filter((template) => template._id !== id)
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
return prev.map((template) =>
|
||||
template._id === id ? merged : template
|
||||
)
|
||||
}
|
||||
|
||||
return [...prev, merged]
|
||||
})
|
||||
},
|
||||
[templateMatchesFilter]
|
||||
)
|
||||
|
||||
updateEventHandlerRef.current = updateEventHandler
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true) return
|
||||
|
||||
const unsubscribe = subscribeToObjectTypeUpdates(
|
||||
'documentTemplate',
|
||||
subscriptionFilter,
|
||||
() => loadDocumentTemplatesRef.current(true)
|
||||
)
|
||||
subscribeToObjectTypeUpdatesRef.current = unsubscribe
|
||||
|
||||
return () => {
|
||||
if (unsubscribe) unsubscribe()
|
||||
if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [connected, subscriptionFilter, subscribeToObjectTypeUpdates])
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true) return
|
||||
|
||||
const templateIds = documentTemplates.map((template) => template._id).filter(Boolean)
|
||||
|
||||
const newTemplateIds = templateIds.filter(
|
||||
(id) => !subscribedIdsRef.current.includes(id)
|
||||
)
|
||||
|
||||
newTemplateIds.forEach((itemId) => {
|
||||
const unsubscribe = subscribeToObjectUpdates(
|
||||
itemId?.toLowerCase(),
|
||||
'documentTemplate',
|
||||
(updateData) => updateEventHandlerRef.current(itemId, updateData)
|
||||
)
|
||||
subscribedIdsRef.current.push(itemId)
|
||||
if (unsubscribe) {
|
||||
unsubscribesRef.current.push(unsubscribe)
|
||||
}
|
||||
})
|
||||
|
||||
const templateIdsToUnsubscribe = subscribedIdsRef.current.filter(
|
||||
(id) => !templateIds.includes(id)
|
||||
)
|
||||
|
||||
templateIdsToUnsubscribe.forEach((itemId) => {
|
||||
const index = subscribedIdsRef.current.indexOf(itemId)
|
||||
if (index > -1) {
|
||||
subscribedIdsRef.current.splice(index, 1)
|
||||
const unsubscribe = unsubscribesRef.current[index]
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
}
|
||||
unsubscribesRef.current.splice(index, 1)
|
||||
}
|
||||
})
|
||||
}, [documentTemplates, connected, subscribeToObjectUpdates])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
unsubscribesRef.current.forEach((unsubscribe) => {
|
||||
if (unsubscribe) unsubscribe()
|
||||
})
|
||||
unsubscribesRef.current = []
|
||||
subscribedIdsRef.current = []
|
||||
|
||||
if (subscribeToObjectTypeUpdatesRef.current) {
|
||||
subscribeToObjectTypeUpdatesRef.current()
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Handle template selection
|
||||
const handleTemplateSelect = (template) => {
|
||||
setCurrentDocumentTemplate(template)
|
||||
@ -78,13 +205,54 @@ const DocumentPrintButton = ({
|
||||
// This could open a print dialog, navigate to a print page, etc.
|
||||
}
|
||||
|
||||
// Create dropdown menu items
|
||||
const menuItems = documentTemplates.map((template) => ({
|
||||
key: template._id,
|
||||
label: template.name,
|
||||
icon: <DocumentTemplateIcon />,
|
||||
onClick: () => handleTemplateSelect(template)
|
||||
}))
|
||||
// Group templates by tag for nested dropdown menu; untagged templates at root
|
||||
const menuItems = useMemo(() => {
|
||||
const templatesByTag = new Map()
|
||||
const untaggedTemplates = []
|
||||
|
||||
const toTemplateMenuItem = (template) => ({
|
||||
key: template._id,
|
||||
label: template.name,
|
||||
icon: <DocumentTemplateIcon />,
|
||||
onClick: () => handleTemplateSelect(template)
|
||||
})
|
||||
|
||||
for (const template of documentTemplates) {
|
||||
const templateTags = Array.isArray(template.tags)
|
||||
? template.tags.filter(Boolean)
|
||||
: []
|
||||
|
||||
if (templateTags.length === 0) {
|
||||
untaggedTemplates.push(template)
|
||||
continue
|
||||
}
|
||||
|
||||
for (const tag of templateTags) {
|
||||
const templates = templatesByTag.get(tag) ?? []
|
||||
templates.push(template)
|
||||
templatesByTag.set(tag, templates)
|
||||
}
|
||||
}
|
||||
|
||||
const taggedMenuItems = [...templatesByTag.keys()]
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((tag) => ({
|
||||
key: tag,
|
||||
label: tag,
|
||||
children: templatesByTag
|
||||
.get(tag)
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(toTemplateMenuItem)
|
||||
}))
|
||||
|
||||
const rootUntaggedItems = untaggedTemplates
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(toTemplateMenuItem)
|
||||
|
||||
return [...rootUntaggedItems, ...taggedMenuItems]
|
||||
}, [documentTemplates])
|
||||
|
||||
// If no templates available, show disabled state
|
||||
if (documentTemplates.length === 0 && !loading) {
|
||||
|
||||
@ -278,7 +278,11 @@ const ObjectProperty = ({
|
||||
return (
|
||||
<Tree
|
||||
treeData={value}
|
||||
fieldNames={{ title: 'title', key: 'value', children: 'children' }}
|
||||
fieldNames={{
|
||||
title: 'title',
|
||||
key: 'value',
|
||||
children: 'children'
|
||||
}}
|
||||
height={320}
|
||||
virtual
|
||||
defaultExpandAll={false}
|
||||
@ -531,6 +535,8 @@ const ObjectProperty = ({
|
||||
case 'state': {
|
||||
if (value && value?.type) {
|
||||
return <StateDisplay {...rest} state={value} />
|
||||
} else if (value && value?.message) {
|
||||
return <Text {...textParams}>{value.message}</Text>
|
||||
} else {
|
||||
return (
|
||||
<Text type='secondary' {...textParams}>
|
||||
|
||||
@ -23,10 +23,13 @@ const UserNotifierToggle = memo(({
|
||||
toggleUserNotifier,
|
||||
editUserNotifier,
|
||||
fetchUserNotifiersForObject,
|
||||
fetchAllUserNotifiersForObject
|
||||
fetchAllUserNotifiersForObject,
|
||||
connected,
|
||||
subscribeToObjectUpdates,
|
||||
subscribeToObjectTypeUpdates
|
||||
} = useContext(ApiServerContext)
|
||||
const { userProfile, token, authInitialized } = useContext(AuthContext)
|
||||
const [isNotifying, setIsNotifying] = useState(false)
|
||||
const [currentUserNotifier, setCurrentUserNotifier] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialLoad, setInitialLoad] = useState(true)
|
||||
const [allNotifiers, setAllNotifiers] = useState([])
|
||||
@ -34,8 +37,32 @@ const UserNotifierToggle = memo(({
|
||||
const [popoverLoading, setPopoverLoading] = useState(false)
|
||||
const [emailTogglingId, setEmailTogglingId] = useState(null)
|
||||
|
||||
const subscribedIdsRef = useRef([])
|
||||
const unsubscribesRef = useRef([])
|
||||
const subscribeToObjectTypeUpdatesRef = useRef(null)
|
||||
const updateEventHandlerRef = useRef()
|
||||
const popoverOpenRef = useRef(popoverOpen)
|
||||
popoverOpenRef.current = popoverOpen
|
||||
|
||||
const objectId = objectData?._id
|
||||
const authReady = Boolean(token && authInitialized)
|
||||
const isNotifying = Boolean(currentUserNotifier)
|
||||
|
||||
const subscriptionFilter = useMemo(
|
||||
() => ({
|
||||
object: objectId,
|
||||
objectType: type
|
||||
}),
|
||||
[objectId, type]
|
||||
)
|
||||
|
||||
const notifierMatchesFilter = useCallback(
|
||||
(notifier) => {
|
||||
const objectRef = notifier?.object?._id ?? notifier?.object
|
||||
return objectRef === objectId && notifier?.objectType === type
|
||||
},
|
||||
[objectId, type]
|
||||
)
|
||||
|
||||
const apiRef = useRef({
|
||||
fetchUserNotifiersForObject,
|
||||
@ -46,42 +73,196 @@ const UserNotifierToggle = memo(({
|
||||
fetchAllUserNotifiersForObject
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!authReady || !objectId || !type) return
|
||||
const loadNotifierState = useCallback(
|
||||
async (silent = false) => {
|
||||
if (!authReady || !objectId || !type) return
|
||||
|
||||
const loadNotifierState = async () => {
|
||||
setInitialLoad(true)
|
||||
if (!silent) {
|
||||
setInitialLoad(true)
|
||||
}
|
||||
try {
|
||||
const { data } = await apiRef.current.fetchUserNotifiersForObject(objectId, type)
|
||||
setIsNotifying(data?.length > 0)
|
||||
const { data } = await apiRef.current.fetchUserNotifiersForObject(
|
||||
objectId,
|
||||
type
|
||||
)
|
||||
setCurrentUserNotifier(data?.[0] ?? null)
|
||||
} catch (error) {
|
||||
console.error('Error fetching user notifier state:', error)
|
||||
} finally {
|
||||
setInitialLoad(false)
|
||||
if (!silent) {
|
||||
setInitialLoad(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[authReady, objectId, type]
|
||||
)
|
||||
|
||||
loadNotifierState()
|
||||
}, [authReady, objectId, type])
|
||||
const loadAllNotifiers = useCallback(
|
||||
async (silent = false) => {
|
||||
if (!authReady || !objectId || !type) return
|
||||
|
||||
useEffect(() => {
|
||||
if (!authReady || !objectId || !type || !popoverOpen) return
|
||||
|
||||
const loadAllNotifiers = async () => {
|
||||
setPopoverLoading(true)
|
||||
if (!silent) {
|
||||
setPopoverLoading(true)
|
||||
}
|
||||
try {
|
||||
const { data } = await apiRef.current.fetchAllUserNotifiersForObject(objectId, type)
|
||||
const { data } = await apiRef.current.fetchAllUserNotifiersForObject(
|
||||
objectId,
|
||||
type
|
||||
)
|
||||
setAllNotifiers(data || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching all user notifiers:', error)
|
||||
setAllNotifiers([])
|
||||
} finally {
|
||||
setPopoverLoading(false)
|
||||
if (!silent) {
|
||||
setPopoverLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[authReady, objectId, type]
|
||||
)
|
||||
|
||||
const loadNotifierStateRef = useRef(loadNotifierState)
|
||||
loadNotifierStateRef.current = loadNotifierState
|
||||
|
||||
const loadAllNotifiersRef = useRef(loadAllNotifiers)
|
||||
loadAllNotifiersRef.current = loadAllNotifiers
|
||||
|
||||
useEffect(() => {
|
||||
loadNotifierState()
|
||||
}, [loadNotifierState])
|
||||
|
||||
useEffect(() => {
|
||||
if (!popoverOpen) return
|
||||
loadAllNotifiers()
|
||||
}, [popoverOpen, loadAllNotifiers])
|
||||
|
||||
const updateEventHandler = useCallback(
|
||||
(id, updatedData) => {
|
||||
setAllNotifiers((prev) => {
|
||||
const existing = prev.find((notifier) => notifier._id === id)
|
||||
const merged = existing
|
||||
? { ...existing, ...updatedData }
|
||||
: { _id: id, ...updatedData }
|
||||
|
||||
if (!notifierMatchesFilter(merged)) {
|
||||
return prev.filter((notifier) => notifier._id !== id)
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
return prev.map((notifier) =>
|
||||
notifier._id === id ? merged : notifier
|
||||
)
|
||||
}
|
||||
|
||||
return [...prev, merged]
|
||||
})
|
||||
|
||||
setCurrentUserNotifier((prev) => {
|
||||
if (prev?._id === id) {
|
||||
const merged = { ...prev, ...updatedData }
|
||||
return notifierMatchesFilter(merged) ? merged : null
|
||||
}
|
||||
|
||||
const userRef = updatedData?.user?._id ?? updatedData?.user
|
||||
if (
|
||||
userRef === userProfile?._id &&
|
||||
notifierMatchesFilter({ _id: id, ...updatedData })
|
||||
) {
|
||||
return { _id: id, ...updatedData }
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
},
|
||||
[notifierMatchesFilter, userProfile?._id]
|
||||
)
|
||||
|
||||
updateEventHandlerRef.current = updateEventHandler
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true || !objectId || !type) return
|
||||
|
||||
const unsubscribe = subscribeToObjectTypeUpdates(
|
||||
'userNotifier',
|
||||
subscriptionFilter,
|
||||
() => {
|
||||
loadNotifierStateRef.current(true)
|
||||
if (popoverOpenRef.current) {
|
||||
loadAllNotifiersRef.current(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
subscribeToObjectTypeUpdatesRef.current = unsubscribe
|
||||
|
||||
return () => {
|
||||
if (unsubscribe) unsubscribe()
|
||||
if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [connected, subscriptionFilter, subscribeToObjectTypeUpdates, objectId, type])
|
||||
|
||||
loadAllNotifiers()
|
||||
}, [authReady, objectId, type, popoverOpen])
|
||||
const subscribedNotifierIds = useMemo(() => {
|
||||
const ids = new Set(
|
||||
allNotifiers.map((notifier) => notifier._id).filter(Boolean)
|
||||
)
|
||||
if (currentUserNotifier?._id) {
|
||||
ids.add(currentUserNotifier._id)
|
||||
}
|
||||
return [...ids]
|
||||
}, [allNotifiers, currentUserNotifier])
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true) return
|
||||
|
||||
const newNotifierIds = subscribedNotifierIds.filter(
|
||||
(id) => !subscribedIdsRef.current.includes(id)
|
||||
)
|
||||
|
||||
newNotifierIds.forEach((itemId) => {
|
||||
const unsubscribe = subscribeToObjectUpdates(
|
||||
itemId?.toLowerCase(),
|
||||
'userNotifier',
|
||||
(updateData) => updateEventHandlerRef.current(itemId, updateData)
|
||||
)
|
||||
subscribedIdsRef.current.push(itemId)
|
||||
if (unsubscribe) {
|
||||
unsubscribesRef.current.push(unsubscribe)
|
||||
}
|
||||
})
|
||||
|
||||
const notifierIdsToUnsubscribe = subscribedIdsRef.current.filter(
|
||||
(id) => !subscribedNotifierIds.includes(id)
|
||||
)
|
||||
|
||||
notifierIdsToUnsubscribe.forEach((itemId) => {
|
||||
const index = subscribedIdsRef.current.indexOf(itemId)
|
||||
if (index > -1) {
|
||||
subscribedIdsRef.current.splice(index, 1)
|
||||
const unsubscribe = unsubscribesRef.current[index]
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
}
|
||||
unsubscribesRef.current.splice(index, 1)
|
||||
}
|
||||
})
|
||||
}, [subscribedNotifierIds, connected, subscribeToObjectUpdates])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
unsubscribesRef.current.forEach((unsubscribe) => {
|
||||
if (unsubscribe) unsubscribe()
|
||||
})
|
||||
unsubscribesRef.current = []
|
||||
subscribedIdsRef.current = []
|
||||
|
||||
if (subscribeToObjectTypeUpdatesRef.current) {
|
||||
subscribeToObjectTypeUpdatesRef.current()
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
if (!authReady || !objectId || !type || loading) return
|
||||
@ -89,7 +270,12 @@ const UserNotifierToggle = memo(({
|
||||
setLoading(true)
|
||||
try {
|
||||
const enabled = await toggleUserNotifier(objectId, type)
|
||||
setIsNotifying(enabled)
|
||||
if (enabled) {
|
||||
const { data } = await fetchUserNotifiersForObject(objectId, type)
|
||||
setCurrentUserNotifier(data?.[0] ?? null)
|
||||
} else {
|
||||
setCurrentUserNotifier(null)
|
||||
}
|
||||
if (popoverOpen) {
|
||||
const { data } = await fetchAllUserNotifiersForObject(objectId, type)
|
||||
setAllNotifiers(data || [])
|
||||
@ -105,7 +291,7 @@ const UserNotifierToggle = memo(({
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [authReady, objectId, type, loading, popoverOpen, toggleUserNotifier, fetchAllUserNotifiersForObject])
|
||||
}, [authReady, objectId, type, loading, popoverOpen, toggleUserNotifier, fetchUserNotifiersForObject, fetchAllUserNotifiersForObject])
|
||||
|
||||
const getUserDisplayName = useCallback((user) => {
|
||||
if (!user) return 'Unknown'
|
||||
@ -132,6 +318,11 @@ const UserNotifierToggle = memo(({
|
||||
n._id === item._id ? { ...n, email: result.email ?? newEmail } : n
|
||||
)
|
||||
)
|
||||
setCurrentUserNotifier((prev) =>
|
||||
prev?._id === item._id
|
||||
? { ...prev, email: result.email ?? newEmail }
|
||||
: prev
|
||||
)
|
||||
message.success(
|
||||
(result.email ?? newEmail)
|
||||
? 'Email notifications enabled'
|
||||
|
||||
@ -2408,8 +2408,8 @@ const ApiServerProvider = ({ children }) => {
|
||||
if (!userProfile?._id) return { data: [] }
|
||||
const result = await fetchObjects('userNotifier', {
|
||||
filter: {
|
||||
'user._id': userProfile._id,
|
||||
'object._id': objectId,
|
||||
user: userProfile._id,
|
||||
object: objectId,
|
||||
objectType
|
||||
},
|
||||
limit: 1
|
||||
@ -2420,7 +2420,7 @@ const ApiServerProvider = ({ children }) => {
|
||||
const fetchAllUserNotifiersForObject = async (objectId, objectType) => {
|
||||
const result = await fetchObjects('userNotifier', {
|
||||
filter: {
|
||||
'object._id': objectId,
|
||||
object: objectId,
|
||||
objectType
|
||||
},
|
||||
limit: 100
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user