Skip to content

Commit f2ecceb

Browse files
karlitschekclaude
authored andcommitted
feat(sidebar): page through older activities instead of stopping at 50
The panel sent no `limit` and no `since`, so it received the API default of 50 activities and had no way to ask for the rest. For any file with a longer history the list simply ended, with nothing indicating that it had been cut short. The page size is now requested explicitly rather than inherited from the API default, and a "Load older activities" action at the end of the list fetches the next page and appends it. Whether more activities remain is taken from the `Link: rel="next"` header rather than guessed from the size of the page. The server builds that header only when its `has_more` probe found an extra row, so it is exact, whereas a full page is not evidence that another one exists. The cursor comes from `X-Activity-Last-Given`, as in the stream view. A 304 on a later page is treated as the end of the list, not as a failure. Activities contributed by other apps through `registerSidebarEntries()` describe the node rather than a page, so they are collected once per file and re-merged into each page instead of being requested again per page. Selecting another file drops the cursor along with the rest of the state, since carrying it over would skip the newest activities of the new file. The axios test mock now returns response headers alongside the body, which is what the real client does and what pagination reads. Signed-off-by: Frank Karlitschek <karlitschek@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4ad419e commit f2ecceb

9 files changed

Lines changed: 230 additions & 23 deletions

js/ActivityTab-Cj5ERLj2.chunk.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".download-summary[data-v-81e4514f]{display:flex;align-items:flex-start;padding:8px 0;margin-bottom:calc(var(--default-grid-baseline) * 2);color:var(--color-text-maxcontrast)}.download-summary__icon[data-v-81e4514f]{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;margin-top:2px;opacity:.5}.download-summary__text[data-v-81e4514f]{padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activity[data-v-242e244d]{display:flex;flex-direction:column;overflow:hidden;height:100%}.activity__actions[data-v-242e244d]{display:flex;flex-direction:column;width:100%}.activity__list[data-v-242e244d]{flex-grow:1;overflow:scroll}.activity__load-more[data-v-242e244d]{display:flex;justify-content:center;padding-block:calc(var(--default-grid-baseline) * 2)}.activity__empty-content[data-v-242e244d]{height:100%}[data-v-242e244d] .empty-content__icon span{background-size:64px;width:64px;height:64px}")),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
2+
import{l as R}from"./activity-sidebar.mjs";import{b as M,m as S,h as P,o as D,N as V}from"./NcCheckboxRadioSwitch-BVTMQSAg-TXGiNHsj.chunk.mjs";import{d as C,x as q,B,y as T,a,c as o,j as H,X as p,J as f,m as n,e as k,f as c,t as I,E as y,l as W,b as d,g as u,F as g,K as G,W as F,h as O,n as U}from"./translation-DoG5ZELJ-BncYkOr6.chunk.mjs";import{a as N}from"./index-BrNGPgve.chunk.mjs";import{j as z,f as J,A as K}from"./ActivityComponent.vue_vue_type_script_setup_true_lang-D7FCSQUD.chunk.mjs";import{l as w}from"./logger-CXg3FpL2.chunk.mjs";import{g as X,a as Z,b as $}from"./api-fwrRGLr6.chunk.mjs";import"./preload-helper-DxYC2qmj.chunk.mjs";var b;(function(t){t[t.User=0]="User",t[t.Group=1]="Group",t[t.Link=3]="Link",t[t.Email=4]="Email",t[t.Remote=6]="Remote",t[t.Team=7]="Team",t[t.Guest=8]="Guest",t[t.RemoteGroup=9]="RemoteGroup",t[t.Room=10]="Room",t[t.Deck=12]="Deck",t[t.FederatedGroup=14]="FederatedGroup",t[t.ScienceMesh=15]="ScienceMesh"})(b||(b={}));const Q=C({__name:"ActivitySidebarPlugin",props:{plugin:{},node:{}},emits:["reloadActivities"],setup(t,{emit:e}){const i=t,s=e,r=H();return q(()=>i.plugin.mount(r.value,{node:i.node,context:B()?.proxy??void 0,reload:()=>s("reloadActivities")})),T(()=>i.plugin.unmount()),(m,l)=>(a(),o("div",{ref_key:"attachTarget",ref:r},null,512))}}),Y='<svg xmlns="http://www.w3.org/2000/svg" id="mdi-download-circle" viewBox="0 0 24 24"><path d="M12 2C17.5 2 22 6.5 22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2M8 17H16V15H8V17M16 10H13.5V6H10.5V10H8L12 14L16 10Z" /></svg>',tt=C({name:"DownloadSummary",components:{NcIconSvgWrapper:M},props:{fileId:{type:Number,required:!0}},data(){return{totalCount:0,monthlyCount:0,downloadSVG:Y}},computed:{summaryText(){return this.monthlyCount>0&&this.monthlyCount<this.totalCount?p("activity","Downloaded %n time (%s in the last 30 days)","Downloaded %n times (%s in the last 30 days)",this.totalCount,[String(this.monthlyCount)]):p("activity","Downloaded %n time","Downloaded %n times",this.totalCount)}},watch:{fileId:{immediate:!0,handler(){this.fetchCounts()}}},methods:{async fetchCounts(){if(this.fileId){this.totalCount=0,this.monthlyCount=0;try{const t=await S.get(N("apps/activity/api/v2/activity/downloads/count"),{params:{format:"json",object_type:"files",object_id:this.fileId}});this.totalCount=t.data.ocs.data.total,this.monthlyCount=t.data.ocs.data.last30d}catch(t){w.error("Failed to fetch download counts",{error:t})}}},t:f,n:p}}),it={key:0,class:"download-summary"},et={class:"download-summary__icon"},at={class:"download-summary__text"};function st(t,e,i,s,r,m){const l=n("NcIconSvgWrapper");return t.totalCount>0?(a(),o("div",it,[k("span",et,[c(l,{svg:t.downloadSVG,size:20},null,8,["svg"])]),k("span",at,I(t.summaryText),1)])):y("",!0)}const ot=P(tt,[["render",st],["__scopeId","data-v-81e4514f"]]),nt=50,rt=C({name:"ActivityTab",components:{ActivityComponent:J,DownloadSummary:ot,NcButton:V,NcEmptyContent:z,NcIconSvgWrapper:M,NcLoadingIcon:D,ActivitySidebarPlugin:Q},props:{node:{type:Object,required:!0},folder:{type:Object,required:!1,default:void 0},view:{type:Object,required:!1,default:void 0}},expose:["update"],data(){return{error:"",loading:!0,loadingMore:!1,activities:[],lightningBoltSVG:R,sidebarPlugins:[],requestController:null,realActivities:[],otherEntries:[],lastGiven:void 0,hasMore:!1}},computed:{hasPublicLink(){return Object.values(this.node?.attributes?.["share-types"]??{}).flat().includes(b.Link)}},watch:{node:{immediate:!0,async handler(){await this.update()}}},beforeUnmount(){this.requestController?.abort()},methods:{async update(){this.sidebarPlugins=[];const t=$();t.length>0&&W(()=>{this.sidebarPlugins=t}),this.resetState(),await this.getActivities()},async getActivities(){await this.fetchPage(!0)},async loadMoreActivities(){await this.fetchPage(!1)},async fetchPage(t){this.requestController?.abort();const e=new AbortController;this.requestController=e;const{signal:i}=e;try{t?this.loading=!0:this.loadingMore=!0;const s=await this.loadRealActivities(i,t?void 0:this.lastGiven),r=this.processActivities(s.activities),m=t?await Z({node:this.node}):this.otherEntries;if(i.aborted)return;this.realActivities=t?r:[...this.realActivities,...r],this.otherEntries=m,(t||s.lastGiven!==void 0)&&(this.lastGiven=s.lastGiven),this.hasMore=s.hasMore,this.activities=[...this.realActivities,...this.otherEntries].sort((l,h)=>h.timestamp-l.timestamp)}catch(s){if(i.aborted)return;this.error=f("activity","Unable to load the activity list"),w.error("Error loading the activity list",{error:s})}finally{i.aborted||(this.loading=!1,this.loadingMore=!1)}},resetState(){this.loading=!0,this.loadingMore=!1,this.error="",this.activities=[],this.realActivities=[],this.otherEntries=[],this.lastGiven=void 0,this.hasMore=!1},async loadRealActivities(t,e){try{const i=await S.get(N("apps/activity/api/v2/activity/filter"),{signal:t,params:{format:"json",object_type:"files",object_id:this.node.fileid,limit:nt,...e===void 0?{}:{since:e}}});return{activities:i.data.ocs.data,lastGiven:i.headers["x-activity-last-given"],hasMore:String(i.headers.link??"").includes('rel="next"')}}catch(i){if(i.response!==void 0&&i.response.status===304)return{activities:[],lastGiven:void 0,hasMore:!1};throw i}},processActivities(t){t=t.map(i=>new K(i)),w.debug(`Processed ${t.length} activity(ies)`,{activities:t,node:this.node});const e=X();return t.filter(i=>!e||e.every(s=>s(i)))},t:f}}),lt={key:0,class:"activity__actions"},dt={key:4,class:"activity__list"},ct={key:0,class:"activity__load-more"};function ht(t,e,i,s,r,m){const l=n("NcIconSvgWrapper"),h=n("NcEmptyContent"),j=n("ActivitySidebarPlugin"),x=n("DownloadSummary"),_=n("NcLoadingIcon"),L=n("ActivityComponent"),E=n("NcButton");return a(),o("div",{class:U([{"icon-loading":t.loading},"activity"])},[t.error||!t.node?(a(),d(h,{key:0,name:t.error},{icon:u(()=>[c(l,{svg:t.lightningBoltSVG},null,8,["svg"])]),_:1},8,["name"])):(a(),o(g,{key:1},[t.sidebarPlugins.length>0?(a(),o("div",lt,[(a(!0),o(g,null,G(t.sidebarPlugins,(v,A)=>(a(),d(j,{key:A,plugin:v,node:t.node,onReloadActivities:e[0]||(e[0]=vt=>t.getActivities())},null,8,["plugin","node"]))),128))])):y("",!0),t.hasPublicLink&&t.node.fileid?(a(),d(x,{key:1,fileId:t.node.fileid},null,8,["fileId"])):y("",!0),t.loading?(a(),d(h,{key:2,class:"activity__empty-content",name:t.t("activity","Loading activities")},{icon:u(()=>[c(_)]),_:1},8,["name"])):t.activities.length===0?(a(),d(h,{key:3,class:"activity__empty-content",name:t.t("activity","No activity yet")},{icon:u(()=>[c(l,{svg:t.lightningBoltSVG},null,8,["svg"])]),_:1},8,["name"])):(a(),o("ul",dt,[(a(!0),o(g,null,G(t.activities,v=>(a(),d(L,{key:v.id,activity:v,showPreviews:!1,onReload:e[1]||(e[1]=A=>t.getActivities())},null,8,["activity"]))),128)),t.hasMore?(a(),o("li",ct,[c(E,{variant:"tertiary",wide:"",disabled:t.loadingMore,onClick:e[2]||(e[2]=v=>t.loadMoreActivities())},F({default:u(()=>[O(" "+I(t.loadingMore?t.t("activity","Loading older activities…"):t.t("activity","Load older activities")),1)]),_:2},[t.loadingMore?{name:"icon",fn:u(()=>[c(_,{size:20})]),key:"0"}:void 0]),1032,["disabled"])])):y("",!0)]))],64))],2)}const Ct=P(rt,[["render",ht],["__scopeId","data-v-242e244d"]]);export{Ct as default};
3+
//# sourceMappingURL=ActivityTab-Cj5ERLj2.chunk.mjs.map

js/ActivityTab-Cj5ERLj2.chunk.mjs.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/ActivityTab-DweEORmo.chunk.mjs

Lines changed: 0 additions & 3 deletions
This file was deleted.

js/ActivityTab-DweEORmo.chunk.mjs.map

Lines changed: 0 additions & 1 deletion
This file was deleted.

js/activity-sidebar.mjs

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

src/__mocks__/@nextcloud/axios.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@ const axios = {
1212
get(url) {
1313
return new Promise((resolve, reject) => {
1414
if (url.endsWith('/ocs/v2.php/apps/activity/api/v2/activity/filter')) {
15-
resolve({ data: wsData })
15+
// Real responses carry the pagination headers; no `link` here
16+
// because this fixture is the whole list
17+
resolve({
18+
data: wsData,
19+
headers: {
20+
'x-activity-last-given': String(wsData.ocs.data.at(-1).activity_id),
21+
},
22+
})
1623
} else if (url.endsWith('/ocs/v2.php/apps/activity/api/v2/activity/downloads/count')) {
17-
resolve({ data: { ocs: { meta: { status: 'ok', statuscode: 200 }, data: { total: 10, last30d: 3 } } } })
24+
resolve({ data: { ocs: { meta: { status: 'ok', statuscode: 200 }, data: { total: 10, last30d: 3 } } }, headers: {} })
1825
} else {
1926
reject(new Error(`URL not defined ${url}`))
2027
}

0 commit comments

Comments
 (0)