Skip to content

Commit

Permalink
Activities (#10996)
Browse files Browse the repository at this point in the history
* add activities panel

* fix: use variable font version of inter woff2

* fix: fetch activities on resource update

* fix: make activities available in shares

* Fix unit tests

---------

Co-authored-by: Benedikt Kulmann <[email protected]>
  • Loading branch information
AlexAndBear and kulmann committed Jun 27, 2024
1 parent cb79022 commit 8c65bcf
Show file tree
Hide file tree
Showing 17 changed files with 1,830 additions and 434 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Enhancement: add action drop down top app top bar
Enhancement: add action drop down to app top bar

We've added an action drop down with various file actions to the app top bar,
so the user can now call different actions like download, directly from the app.
Expand Down
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-add-activities-panel
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Activities sidebar app panel
We have added a sidebar app panel to display activities performed on a resource.
This provides a clear overview of actions taken on a resource.

https://github.com/owncloud/web/pull/10996
https://github.com/owncloud/web/issues/10800
Binary file modified packages/design-system/src/assets/fonts/inter.woff2
Binary file not shown.
177 changes: 177 additions & 0 deletions packages/web-app-files/src/components/SideBar/ActivitiesPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<template>
<div ref="rootElement">
<app-loading-spinner v-if="isLoading" />
<template v-else>
<p v-if="!activities.length" v-text="$gettext('No activities available for this resource')" />
<div v-else class="oc-ml-s">
<ul class="timeline">
<li v-for="activity in activities" :key="activity.id">
<span v-html="getHtmlFromActivity(activity)" />
<span
class="oc-text-muted oc-text-small oc-mt-s"
v-text="getTimeFromActivity(activity)"
/>
</li>
</ul>
<p class="oc-text-muted oc-text-small" v-text="activitiesLimitText" />
</div>
</template>
</div>
</template>

<script lang="ts">
import {
computed,
defineComponent,
inject,
onBeforeUnmount,
onMounted,
Ref,
ref,
unref,
watch
} from 'vue'
import { useGettext } from 'vue3-gettext'
import {
AppLoadingSpinner,
formatDateFromDateTime,
useClientService,
VisibilityObserver
} from '@ownclouders/web-pkg'
import { useTask } from 'vue-concurrency'
import { call, Resource } from '@ownclouders/web-client'
import { DateTime } from 'luxon'
import { Activity } from '@ownclouders/web-client/graph/generated'
const visibilityObserver = new VisibilityObserver()
export default defineComponent({
name: 'ActivitiesPanel',
components: { AppLoadingSpinner },
setup() {
const rootElement = ref<HTMLElement>()
const { $gettext, current: currentLanguage } = useGettext()
const clientService = useClientService()
const resource = inject<Ref<Resource>>('resource')
const activities = ref<Activity[]>([])
const activitiesLimit = 200
const activitiesLimitText = computed(() => {
return $gettext('(Showing latest %{activitiesLimit} activities)', {
activitiesLimit: activitiesLimit.toString()
})
})
const loadActivitiesTask = useTask(function* (signal) {
const {
data: { value: activitiesResponse }
} = yield* call(
clientService.graphAuthenticated.activities.listActivities(
`itemid:${unref(resource).id} AND limit:${activitiesLimit} AND sort:desc`
)
)
activities.value = activitiesResponse
}).restartable()
const isLoading = computed(() => {
return loadActivitiesTask.isRunning || !loadActivitiesTask.last
})
const getHtmlFromActivity = (activity: Activity) => {
let message = activity.template.message
for (const [key, value] of Object.entries(activity.template.variables)) {
message = message.replace(`{${key}}`, `<strong>${value.displayName || value.name}</strong>`)
}
return message
}
const getTimeFromActivity = (activity: Activity) => {
const dateTime = DateTime.fromISO(activity.times.recordedTime)
return formatDateFromDateTime(dateTime, currentLanguage)
}
const isVisible = ref(false)
watch(
[resource, isVisible],
() => {
if (!unref(isVisible)) {
return
}
loadActivitiesTask.perform()
},
{
immediate: true,
deep: true
}
)
onMounted(() => {
visibilityObserver.observe(unref(rootElement), {
onEnter: () => {
isVisible.value = true
},
onExit: () => {
isVisible.value = false
}
})
})
onBeforeUnmount(() => {
visibilityObserver.disconnect()
})
return {
rootElement,
activities,
activitiesLimit,
activitiesLimitText,
isLoading,
isVisible,
loadActivitiesTask,
getHtmlFromActivity,
getTimeFromActivity
}
}
})
</script>

<style lang="scss">
.timeline {
position: relative;
list-style: none;
padding: 0;
margin: 0;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1.5px;
background-color: var(--oc-color-border);
}
li {
display: flex;
flex-direction: column;
position: relative;
padding: 10px 20px 10px 30px;
width: 100%;
box-sizing: border-box;
&::before {
content: '';
width: 10px;
height: 10px;
background-color: var(--oc-color-border);
border-radius: 50%;
position: absolute;
left: -4px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,6 @@ enum KeyCode {
export default defineComponent({
name: 'TagsSelect',
props: {
/**
* The resource
*/
resource: {
type: Object as PropType<Resource>,
required: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
</li>
</ul>
<div v-else>
<p v-translate data-testid="file-versions-no-versions">No Versions available for this file</p>
<p v-translate data-testid="file-versions-no-versions">No versions available for this file</p>
</div>
</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SharesPanel from '../../components/SideBar/Shares/SharesPanel.vue'
import NoSelection from '../../components/SideBar/NoSelection.vue'
import TrashNoSelection from '../../components/SideBar/TrashNoSelection.vue'
import SpaceActions from '../../components/SideBar/Actions/SpaceActions.vue'
import ActivitiesPanel from '../../components/SideBar/ActivitiesPanel.vue'
import {
SpaceDetails,
SpaceDetailsMultiple,
Expand Down Expand Up @@ -385,6 +386,26 @@ export const useSideBarPanels = (): SidebarPanelExtension<SpaceResource, Resourc
return items?.length === 1 && isProjectSpaceResource(items[0])
}
}
},
{
id: 'com.github.owncloud.web.files.sidebar-panel.activities',
type: 'sidebarPanel',
extensionPointIds: [fileSideBarExtensionPoint.id],
panel: {
name: 'activities',
icon: 'pulse',
title: () => $gettext('Activities'),
component: ActivitiesPanel,
isVisible: ({ items, root }) => {
if (items?.length !== 1) {
return false
}
if (isLocationTrashActive(router, 'files-trash-generic')) {
return false
}
return true
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import ActivitiesPanel from 'web-app-files/src/components/SideBar/ActivitiesPanel.vue'
import { defaultComponentMocks, defaultPlugins, mount } from 'web-test-helpers'
import { AxiosResponse } from 'axios'
import { Resource } from '@ownclouders/web-client'
import { mock } from 'vitest-mock-extended'
import { nextTick } from 'vue'

const defaultActivities = [
{
id: '5380e156-d65e-4024-9691-0f0c1f2796e4',
times: {
recordedTime: '2024-07-29T18:34:40Z'
},
template: {
message: '{user} created {resource}.',
variables: {
user: {
id: '71f9de60-8b24-4cfe-9378-87d47aef0d04',
displayName: 'Marie Curie'
},
resource: {
id: '7f92b0a9-06ad-49dc-890f-0e0a6eb4dea6$e9f01ca3-577f-4d1d-acd4-1cc44149ac25!5fb9f87c-a317-467b-9882-eb9f171564ac',
name: 'new folder'
}
}
}
},
{
id: '5380e156-d65e-4024-9691-0f0c1f2796e4',
times: {
recordedTime: '2023-07-29T18:34:40Z'
},
template: {
message: '{user} moved {resource}.',
variables: {
user: {
id: '71f9de60-8b24-4cfe-9378-87d47aef0d04',
displayName: 'Albert Einstein'
},
resource: {
id: '7f92b0a9-06ad-49dc-890f-0e0a6eb4dea6$e9f01ca3-577f-4d1d-acd4-1cc44149ac25!5fb9f87c-a317-467b-9882-eb9f171564ac',
name: 'textfile.txt'
}
}
}
},
{
id: '5380e156-d65e-4024-9691-0f0c1f2796e4',
times: {
recordedTime: '2022-07-29T18:34:40Z'
},
template: {
message: '{user} deleted {resource}.',
variables: {
user: {
id: '71f9de60-8b24-4cfe-9378-87d47aef0d04',
displayName: 'Robert Oppenheimer'
},
resource: {
id: '7f92b0a9-06ad-49dc-890f-0e0a6eb4dea6$e9f01ca3-577f-4d1d-acd4-1cc44149ac25!5fb9f87c-a317-467b-9882-eb9f171564ac',
name: 'atom plans.pdf'
}
}
}
},
{
id: '5380e156-d65e-4024-9691-0f0c1f2796e4',
times: {
recordedTime: '2021-07-29T18:34:40Z'
},
template: {
message: '{user} removed {resource}.',
variables: {
user: {
id: '71f9de60-8b24-4cfe-9378-87d47aef0d04',
displayName: 'Albert Schweitzer'
},
resource: {
id: '7f92b0a9-06ad-49dc-890f-0e0a6eb4dea6$e9f01ca3-577f-4d1d-acd4-1cc44149ac25!5fb9f87c-a317-467b-9882-eb9f171564ac',
name: 'Selfie.png'
}
}
}
}
]
describe('ActivitiesPanel', () => {
it('should show no activities message if there is no data', async () => {
const { wrapper } = getMountedWrapper({ activities: [] })
wrapper.vm.isVisible = true
await nextTick()
await wrapper.vm.loadActivitiesTask.last
expect(wrapper.html()).toContain('No activities available for this resource')
})
it('should show loading spinner when fetching data', async () => {
const { wrapper } = getMountedWrapper()
wrapper.vm.isVisible = true
await nextTick()
expect(wrapper.find('#app-loading-spinner').exists()).toBeTruthy()
})
it('should render a list of activities when data is present', async () => {
const { wrapper } = getMountedWrapper()
wrapper.vm.isVisible = true
await nextTick()
await wrapper.vm.loadActivitiesTask.last
expect(wrapper.html()).toMatchSnapshot()
})
})

function getMountedWrapper({
activities = defaultActivities
}: {
activities?: any[]
} = {}) {
const mocks = {
...defaultComponentMocks()
}
mocks.$clientService.graphAuthenticated.activities.listActivities.mockResolvedValue({
data: { value: activities }
} as AxiosResponse<any>)

return {
wrapper: mount(ActivitiesPanel, {
global: {
mocks,
plugins: [...defaultPlugins()],
provide: { ...mocks, resource: mock<Resource> }
}
}),
mocks
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('FileVersions', () => {
const { wrapper } = getMountedWrapper({ mountType: shallowMount, versions: [] })
const noVersionsMessageElement = wrapper.find(selectors.noVersionsMessage)

expect(noVersionsMessageElement.text()).toBe('No Versions available for this file')
expect(noVersionsMessageElement.text()).toBe('No versions available for this file')
})

describe('when the file has versions', () => {
Expand Down
Loading

0 comments on commit 8c65bcf

Please sign in to comment.