Skip to content

Commit e3e3b28

Browse files
committed
fix(pastebin): fix scrollTo on public links, show anchor links everywhere
- Include folder fileId in anchor URLs so AppWrapper's replaceInvalidFileRoute preserves scrollTo where possible - Show anchor links on public link views (fall back to current URL) - Read scrollTo from window.location.search before AppWrapper can rewrite the URL (public link resolution may change the fileId, causing replaceInvalidFileRoute to strip other query params) - Add e2e test that opens a public link with scrollTo and verifies the target file is scrolled into view
1 parent 6d07b3e commit e3e3b28

3 files changed

Lines changed: 52 additions & 11 deletions

File tree

packages/web-app-pastebin/src/View.vue

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
:resource="file"
7272
:space="space"
7373
:share-url="shareUrl"
74+
:folder-file-id="resource.fileId"
7475
@loaded="onFileLoaded"
7576
/>
7677
</div>
@@ -80,7 +81,7 @@
8081
</template>
8182

8283
<script setup lang="ts">
83-
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
84+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
8485
import {
8586
FolderResource,
8687
LinkShare,
@@ -91,11 +92,9 @@ import {
9192
urlJoin
9293
} from '@opencloud-eu/web-client'
9394
import {
94-
queryItemAsString,
9595
useClientService,
9696
useConfigStore,
9797
useResourcesStore,
98-
useRouteQuery,
9998
useRouter,
10099
useSharesStore,
101100
contextRouteNameKey
@@ -192,20 +191,18 @@ const deletePastebin = () => {
192191
})
193192
}
194193
195-
const scrollToQuery = useRouteQuery('scrollTo')
196-
const scrollTarget = computed(() => queryItemAsString(unref(scrollToQuery)))
194+
// Read scrollTo before AppWrapper's replaceInvalidFileRoute can strip it from the URL
195+
const initialScrollTarget = new URLSearchParams(window.location.search).get('scrollTo') || ''
197196
const filesLoadedCount = ref(0)
198197
const totalFiles = computed(() => folderResources.value.filter((r) => !r.isFolder).length)
199198
200199
const onFileLoaded = async () => {
201200
filesLoadedCount.value++
202201
if (filesLoadedCount.value < totalFiles.value) return
203-
204-
const target = scrollTarget.value
205-
if (!target) return
202+
if (!initialScrollTarget) return
206203
207204
await nextTick()
208-
scrollToFile(target)
205+
scrollToFile(initialScrollTarget)
209206
}
210207
211208
const loadShares = async (folderResource: Resource) => {

packages/web-app-pastebin/src/components/PastebinFile.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const props = defineProps<{
108108
resource: Resource
109109
space: SpaceResource
110110
shareUrl?: string
111+
folderFileId?: string
111112
}>()
112113
113114
const emit = defineEmits<{ loaded: [] }>()
@@ -117,9 +118,16 @@ const clientService = useClientService()
117118
const { copy, copied } = useClipboard({ legacy: true, copiedDuring: 1500 })
118119
119120
const anchorHref = computed(() => {
120-
if (!props.shareUrl) return ''
121-
const url = new URL(props.shareUrl)
121+
// Use the share URL when available (authenticated view), fall back to the current URL
122+
// so anchor links also work on public link views
123+
const base = props.shareUrl || window.location.href
124+
const url = new URL(base)
122125
url.searchParams.set('scrollTo', props.resource.name)
126+
// fileId of the .ocpb folder is needed so AppWrapper's replaceInvalidFileRoute
127+
// doesn't strip scrollTo when rewriting the URL
128+
if (props.folderFileId) {
129+
url.searchParams.set('fileId', props.folderFileId)
130+
}
123131
return url.toString()
124132
})
125133

packages/web-app-pastebin/tests/e2e/pastebin.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,42 @@ test.describe('public links', () => {
316316

317317
await freshContext.close()
318318
})
319+
320+
test('scrollTo works on public link', async ({ browser }) => {
321+
await pastebin.navigateToPastebin()
322+
const { password } = await pastebin.createPastebin({
323+
files: [
324+
{ name: 'top.py', content: '# this is the top file\n'.repeat(30) },
325+
{ name: 'bottom.js', content: '// this is the bottom file' }
326+
]
327+
})
328+
329+
await pastebin.expectFileVisible('top.py')
330+
await pastebin.expectFileVisible('bottom.js')
331+
332+
// wait for share URL to resolve, then get anchor href for bottom file
333+
const linkIcon = userPage.locator('header a[title="Open public link"]')
334+
await expect(linkIcon).toBeVisible({ timeout: 15000 })
335+
const anchorHref = await pastebin.getAnchorHref('bottom.js')
336+
expect(anchorHref).toContain('scrollTo=bottom.js')
337+
338+
// open the anchor link in a fresh unauthenticated context
339+
const freshContext = await browser.newContext({ ignoreHTTPSErrors: true })
340+
const freshPage = await freshContext.newPage()
341+
await freshPage.goto(anchorHref!, { timeout: 30000 })
342+
343+
if (password) {
344+
const passwordField = freshPage.locator('input[type="password"]')
345+
await expect(passwordField).toBeVisible({ timeout: 15000 })
346+
await passwordField.fill(password)
347+
await freshPage.getByRole('button', { name: 'Continue', exact: true }).click()
348+
}
349+
350+
// the bottom file should be visible (scrolled into view)
351+
await expect(freshPage.locator('[data-item-id="bottom.js"]')).toBeVisible({ timeout: 15000 })
352+
353+
await freshContext.close()
354+
})
319355
})
320356

321357
test.describe('navigation', () => {

0 commit comments

Comments
 (0)