diff --git a/backend/src/seed/factories/posts.js b/backend/src/seed/factories/posts.js index e2bc2ab66..83d5844d7 100644 --- a/backend/src/seed/factories/posts.js +++ b/backend/src/seed/factories/posts.js @@ -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}", diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index 491b3f9e1..9fe957515 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -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 diff --git a/cypress/integration/06.Search.feature b/cypress/integration/06.Search.feature index 0a4450829..71aee608a 100644 --- a/cypress/integration/06.Search.feature +++ b/cypress/integration/06.Search.feature @@ -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 diff --git a/cypress/integration/06.WritePost.feature b/cypress/integration/06.WritePost.feature index 0193e44bf..fed1bbf2f 100644 --- a/cypress/integration/06.WritePost.feature +++ b/cypress/integration/06.WritePost.feature @@ -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 diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js index 12f1b326f..e3d3f3975 100644 --- a/cypress/integration/common/report.js +++ b/cypress/integration/common/report.js @@ -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') diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js index 4809e8a13..1c1981581 100644 --- a/cypress/integration/common/search.js +++ b/cypress/integration/common/search.js @@ -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' ) }) diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 1a9891a7a..8944b7c25 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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', () => { diff --git a/cypress/integration/identifier/PersistentLinks.feature b/cypress/integration/identifier/PersistentLinks.feature new file mode 100644 index 000000000..5ea48ef6a --- /dev/null +++ b/cypress/integration/identifier/PersistentLinks.feature @@ -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 "" + Then I get redirected to "" + 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 "" + Then I get redirected to "" + Examples: + | url | redirectUrl | + | /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk | + | /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays | diff --git a/webapp/components/ContributionForm.vue b/webapp/components/ContributionForm.vue index cf7c28ece..3ef041569 100644 --- a/webapp/components/ContributionForm.vue +++ b/webapp/components/ContributionForm.vue @@ -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 => { diff --git a/webapp/components/PostCard.vue b/webapp/components/PostCard.vue index 8f534f6ff..767835f74 100644 --- a/webapp/components/PostCard.vue +++ b/webapp/components/PostCard.vue @@ -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 } } diff --git a/webapp/components/User.vue b/webapp/components/User.vue index 1c78b34cc..dd176a67d 100644 --- a/webapp/components/User.vue +++ b/webapp/components/User.vue @@ -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 } } } } } diff --git a/webapp/graphql/ModerationListQuery.js b/webapp/graphql/ModerationListQuery.js index d8105e388..940ada6f6 100644 --- a/webapp/graphql/ModerationListQuery.js +++ b/webapp/graphql/ModerationListQuery.js @@ -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 } } } diff --git a/webapp/graphql/UserProfileQuery.js b/webapp/graphql/UserProfileQuery.js index 683f0e3ac..f0d7720ae 100644 --- a/webapp/graphql/UserProfileQuery.js +++ b/webapp/graphql/UserProfileQuery.js @@ -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 diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue index bdb41f8b2..991662350 100644 --- a/webapp/layouts/default.vue +++ b/webapp/layouts/default.vue @@ -39,7 +39,7 @@ > { this.$router.push({ - name: 'post-slug', - params: { slug: item.slug } + name: 'post-id-slug', + params: { id: item.id, slug: item.slug } }) }) }, diff --git a/webapp/mixins/persistentLinks.js b/webapp/mixins/persistentLinks.js new file mode 100644 index 000000000..5cecbbdbd --- /dev/null +++ b/webapp/mixins/persistentLinks.js @@ -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 }) + } + } +} diff --git a/webapp/pages/index.vue b/webapp/pages/index.vue index e28d6b2c3..ee824eb59 100644 --- a/webapp/pages/index.vue +++ b/webapp/pages/index.vue @@ -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() { diff --git a/webapp/pages/moderation/index.vue b/webapp/pages/moderation/index.vue index cd41dc17c..fc5d1fbe6 100644 --- a/webapp/pages/moderation/index.vue +++ b/webapp/pages/moderation/index.vue @@ -14,7 +14,7 @@ slot-scope="scope" >
- + {{ scope.row.post.title | truncate(50) }}
- + {{ scope.row.comment.contentExcerpt | truncate(50) }}
- + {{ scope.row.user.name | truncate(50) }}
@@ -69,7 +69,7 @@ slot="submitter" slot-scope="scope" > - + {{ scope.row.submitter.name }} @@ -79,19 +79,19 @@ > {{ scope.row.post.disabledBy.name | truncate(50) }} {{ scope.row.comment.disabledBy.name | truncate(50) }} {{ scope.row.user.disabledBy.name | truncate(50) }} diff --git a/webapp/pages/post/_slug.vue b/webapp/pages/post/_id.vue similarity index 54% rename from webapp/pages/post/_slug.vue rename to webapp/pages/post/_id.vue index d4a233f0f..21dd4b292 100644 --- a/webapp/pages/post/_slug.vue +++ b/webapp/pages/post/_id.vue @@ -17,35 +17,62 @@