Merge pull request #248 from Human-Connection/240-persistent-links

240 persistent links
This commit is contained in:
Robert Schäfer 2019-03-27 00:42:40 +01:00 committed by GitHub
commit 30fc07e16e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 207 additions and 55 deletions

View File

@ -4,6 +4,7 @@ import uuid from 'uuid/v4'
export default function (params) {
const {
id = uuid(),
slug = '',
title = faker.lorem.sentence(),
content = [
faker.lorem.sentence(),
@ -21,6 +22,7 @@ export default function (params) {
mutation {
CreatePost(
id: "${id}",
slug: "${slug}",
title: "${title}",
content: "${content}",
image: "${image}",

View File

@ -5,6 +5,7 @@ export default function create (params) {
const {
id = uuid(),
name = faker.name.findName(),
slug = '',
email = faker.internet.email(),
password = '1234',
role = 'user',
@ -19,6 +20,7 @@ export default function create (params) {
CreateUser(
id: "${id}",
name: "${name}",
slug: "${slug}",
password: "${password}",
email: "${email}",
avatar: "${avatar}",
@ -29,6 +31,7 @@ export default function create (params) {
) {
id
name
slug
email
avatar
role

View File

@ -8,7 +8,7 @@ Feature: Search
And we have the following posts in our database:
| Author | id | title | content |
| Brianna Wiest | p1 | 101 Essays that will change the way you think | 101 Essays, of course! |
| Brianna Wiest | p1 | No searched for content | will be found in this post, I guarantee |
| Brianna Wiest | p2 | No searched for content | will be found in this post, I guarantee |
Given I am logged in
Scenario: Search for specific words

View File

@ -17,7 +17,7 @@ Feature: Create a post
for active citizenship.
"""
And I click on "Save"
Then I get redirected to "/post/my-first-post/"
Then I get redirected to ".../my-first-post"
And the post was saved successfully
Scenario: See a post on the landing page

View File

@ -71,7 +71,7 @@ When('I click on the author', () => {
})
When('I report the author', () => {
cy.get('.page-name-profile-slug').then(() => {
cy.get('.page-name-profile-id-slug').then(() => {
invokeReportOnElement('.ds-card').then(() => {
cy.get('button')
.contains('Send')

View File

@ -42,9 +42,13 @@ When('I select an entry', () => {
})
Then("I should be on the post's page", () => {
cy.location('pathname').should(
'contain',
'/post/'
)
cy.location('pathname').should(
'eq',
'/post/101-essays-that-will-change-the-way-you-think/'
'/post/p1/101-essays-that-will-change-the-way-you-think'
)
})

View File

@ -86,6 +86,10 @@ Given('my user account has the role {string}', role => {
When('I log out', cy.logout)
When('I visit {string}', page => {
cy.openPage(page)
})
When('I visit the {string} page', page => {
cy.openPage(page)
})
@ -220,7 +224,7 @@ Then('the post shows up on the landing page at position {int}', index => {
})
Then('I get redirected to {string}', route => {
cy.location('pathname').should('contain', route)
cy.location('pathname').should('contain', route.replace('...', ''))
})
Then('the post was saved successfully', () => {

View File

@ -0,0 +1,41 @@
Feature: Persistent Links
As a user
I want all links to carry permanent information that identifies the linked resource
In order to have persistent links even if a part of the URL might change
| | Modifiable | Referenceable | Unique | Purpose |
| -- | -- | -- | -- | -- |
| ID | no | yes | yes | Identity, Traceability, Links |
| Slug | yes | yes | yes | @-Mentions, SEO-friendly URL |
| Name | yes | no | no | Search, self-description |
Background:
Given we have the following user accounts:
| id | name | slug |
| MHNqce98y1 | Stephen Hawking | thehawk |
And we have the following posts in our database:
| id | title | slug |
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
And I have a user account
And I am logged in
Scenario Outline: Link with slug only is valid and gets auto-completed
When I visit "<url>"
Then I get redirected to "<redirectUrl>"
Examples:
| url | redirectUrl |
| /profile/thehawk | /profile/MHNqce98y1/thehawk |
| /post/101-essays | /post/bWBjpkTKZp/101-essays |
Scenario: Link with id only will always point to the same user
When I visit "/profile/MHNqce98y1"
Then I get redirected to "/profile/MHNqce98y1/thehawk"
Scenario Outline: ID takes precedence over slug
When I visit "<url>"
Then I get redirected to "<redirectUrl>"
Examples:
| url | redirectUrl |
| /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk |
| /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays |

View File

@ -111,8 +111,8 @@ export default {
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
this.$router.push({
name: 'post-slug',
params: { slug: result.slug }
name: 'post-id-slug',
params: { id: result.id, slug: result.slug }
})
})
.catch(err => {

View File

@ -106,8 +106,8 @@ export default {
methods: {
href(post) {
return this.$router.resolve({
name: 'post-slug',
params: { slug: post.slug }
name: 'post-id-slug',
params: { id: post.id, slug: post.slug }
}).href
}
}

View File

@ -153,9 +153,9 @@ export default {
return count
},
userLink() {
const { slug } = this.user
if (!slug) return ''
return { name: 'profile-slug', params: { slug } }
const { id, slug } = this.user
if (!(id && slug)) return ''
return { name: 'profile-id-slug', params: { slug, id } }
}
}
}

View File

@ -9,58 +9,69 @@ export default app => {
type
createdAt
submitter {
id
slug
name
disabled
deleted
name
slug
}
user {
name
id
slug
name
disabled
deleted
disabledBy {
id
slug
name
disabled
deleted
}
}
comment {
contentExcerpt
author {
name
id
slug
name
disabled
deleted
}
post {
id
slug
title
disabled
deleted
title
slug
}
disabledBy {
disabled
deleted
id
slug
name
disabled
deleted
}
}
post {
title
id
slug
title
disabled
deleted
author {
id
slug
name
disabled
deleted
name
slug
}
disabledBy {
disabled
deleted
id
slug
name
disabled
deleted
}
}
}

View File

@ -6,6 +6,7 @@ export default app => {
query User($slug: String!, $first: Int, $offset: Int) {
User(slug: $slug) {
id
slug
name
avatar
about
@ -27,8 +28,8 @@ export default app => {
followingCount
following(first: 7) {
id
name
slug
name
avatar
disabled
deleted
@ -49,10 +50,10 @@ export default app => {
followedByCurrentUser
followedBy(first: 7) {
id
slug
name
disabled
deleted
slug
avatar
followedByCount
followedByCurrentUser
@ -87,6 +88,7 @@ export default app => {
}
author {
id
slug
avatar
name
disabled

View File

@ -39,7 +39,7 @@
>
<a
class="avatar-menu-trigger"
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
:href="$router.resolve({name: 'profile-id-slug', params: {id: user.id, slug: user.slug}}).href"
@click.prevent="toggleMenu"
>
<ds-avatar
@ -182,8 +182,8 @@ export default {
goToPost(item) {
this.$nextTick(() => {
this.$router.push({
name: 'post-slug',
params: { slug: item.slug }
name: 'post-id-slug',
params: { id: item.id, slug: item.slug }
})
})
},

View File

@ -0,0 +1,32 @@
export default function(options = {}) {
const { queryId, querySlug, path, message = 'Page not found.' } = options
return {
asyncData: async context => {
const {
params: { id, slug },
redirect,
error,
app: { apolloProvider }
} = context
const idOrSlug = id || slug
const variables = { idOrSlug }
const client = apolloProvider.defaultClient
let response
let resource
response = await client.query({ query: queryId, variables })
resource = response.data[Object.keys(response.data)[0]][0]
if (resource && resource.slug === slug) return // all good
if (resource && resource.slug !== slug) {
return redirect(`/${path}/${resource.id}/${resource.slug}`)
}
response = await client.query({ query: querySlug, variables })
resource = response.data[Object.keys(response.data)[0]][0]
if (resource) return redirect(`/${path}/${resource.id}/${resource.slug}`)
return error({ statusCode: 404, message })
}
}
}

View File

@ -64,8 +64,8 @@ export default {
},
href(post) {
return this.$router.resolve({
name: 'post-slug',
params: { slug: post.slug }
name: 'post-id-slug',
params: { id: post.id, slug: post.slug }
}).href
},
showMoreContributions() {

View File

@ -14,7 +14,7 @@
slot-scope="scope"
>
<div v-if="scope.row.type === 'Post'">
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.post.slug } }">
<nuxt-link :to="{ name: 'post-id-slug', params: { id: scope.row.post.id, slug: scope.row.post.slug } }">
<b>{{ scope.row.post.title | truncate(50) }}</b>
</nuxt-link><br>
<ds-text
@ -25,7 +25,7 @@
</ds-text>
</div>
<div v-else-if="scope.row.type === 'Comment'">
<nuxt-link :to="{ name: 'post-slug', params: { slug: scope.row.comment.post.slug } }">
<nuxt-link :to="{ name: 'post-id-slug', params: { id: scope.row.comment.post.id, slug: scope.row.comment.post.slug } }">
<b>{{ scope.row.comment.contentExcerpt | truncate(50) }}</b>
</nuxt-link><br>
<ds-text
@ -36,7 +36,7 @@
</ds-text>
</div>
<div v-else>
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.user.slug } }">
<nuxt-link :to="{ name: 'profile-id-slug', params: { id: scope.row.user.id, slug: scope.row.user.slug } }">
<b>{{ scope.row.user.name | truncate(50) }}</b>
</nuxt-link>
</div>
@ -69,7 +69,7 @@
slot="submitter"
slot-scope="scope"
>
<nuxt-link :to="{ name: 'profile-slug', params: { slug: scope.row.submitter.slug } }">
<nuxt-link :to="{ name: 'profile-id-slug', params: { id: scope.row.submitter.id, slug: scope.row.submitter.slug } }">
{{ scope.row.submitter.name }}
</nuxt-link>
</template>
@ -79,19 +79,19 @@
>
<nuxt-link
v-if="scope.row.type === 'Post' && scope.row.post.disabledBy"
:to="{ name: 'profile-slug', params: { slug: scope.row.post.disabledBy.slug } }"
:to="{ name: 'profile-id-slug', params: { id: scope.row.post.disabledBy.id, slug: scope.row.post.disabledBy.slug } }"
>
<b>{{ scope.row.post.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<nuxt-link
v-else-if="scope.row.type === 'Comment' && scope.row.comment.disabledBy"
:to="{ name: 'profile-slug', params: { slug: scope.row.comment.disabledBy.slug } }"
:to="{ name: 'profile-id-slug', params: { id: scope.row.comment.disabledBy.id, slug: scope.row.comment.disabledBy.slug } }"
>
<b>{{ scope.row.comment.disabledBy.name | truncate(50) }}</b>
</nuxt-link>
<nuxt-link
v-else-if="scope.row.type === 'User' && scope.row.user.disabledBy"
:to="{ name: 'profile-slug', params: { slug: scope.row.user.disabledBy.slug } }"
:to="{ name: 'profile-id-slug', params: { id: scope.row.user.disabledBy.id, slug: scope.row.user.disabledBy.slug } }"
>
<b>{{ scope.row.user.disabledBy.name | truncate(50) }}</b>
</nuxt-link>

View File

@ -17,35 +17,62 @@
</template>
<script>
import gql from 'graphql-tag'
import PersistentLinks from '~/mixins/persistentLinks.js'
const options = {
queryId: gql`
query($idOrSlug: ID) {
Post(id: $idOrSlug) {
id
slug
}
}
`,
querySlug: gql`
query($idOrSlug: String) {
Post(slug: $idOrSlug) {
id
slug
}
}
`,
path: 'post',
message: 'This post could not be found'
}
const persistentLinks = PersistentLinks(options)
export default {
mixins: [persistentLinks],
computed: {
routes() {
const { slug, id } = this.$route.params
return [
{
name: this.$t('common.post', null, 1),
path: `/post/${this.$route.params.slug}`,
path: `/post/${id}/${slug}`,
children: [
{
name: this.$t('common.comment', null, 2),
path: `/post/${this.$route.params.slug}#comments`
path: `/post/${id}/${slug}#comments`
},
{
name: this.$t('common.letsTalk'),
path: `/post/${this.$route.params.slug}#lets-talk`
path: `/post/${id}/${slug}#lets-talk`
},
{
name: this.$t('common.versus'),
path: `/post/${this.$route.params.slug}#versus`
path: `/post/${id}/${slug}#versus`
}
]
},
{
name: this.$t('common.moreInfo'),
path: `/post/${this.$route.params.slug}/more-info`
path: `/post/${id}/${slug}/more-info`
},
{
name: this.$t('common.takeAction'),
path: `/post/${this.$route.params.slug}/take-action`
path: `/post/${id}/${slug}/take-action`
}
]
}

View File

@ -263,7 +263,7 @@ export default {
</script>
<style lang="scss">
.page-name-post-slug {
.page-name-post-id-slug {
.content-menu {
float: right;
margin-right: -$space-x-small;

View File

@ -0,0 +1,34 @@
<template>
<nuxt-child />
</template>
<script>
import gql from 'graphql-tag'
import PersistentLinks from '~/mixins/persistentLinks.js'
const options = {
queryId: gql`
query($idOrSlug: ID) {
User(id: $idOrSlug) {
id
slug
}
}
`,
querySlug: gql`
query($idOrSlug: String) {
User(slug: $idOrSlug) {
id
slug
}
}
`,
message: 'This user could not be found',
path: 'profile'
}
const persistentLinks = PersistentLinks(options)
export default {
mixins: [persistentLinks]
}
</script>

View File

@ -423,7 +423,7 @@ export default {
border: #fff 5px solid;
}
.page-name-profile-slug {
.page-name-profile-id-slug {
.ds-flex-item:first-child .content-menu {
position: absolute;
top: $space-x-small;

View File

@ -1,8 +0,0 @@
<script>
export default {
layout: 'blank',
asyncData({ error }) {
error({ statusCode: 404, message: 'Profile slug missing' })
}
}
</script>