mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' into map
This commit is contained in:
commit
b8228db9f3
107
CONTRIBUTING.md
107
CONTRIBUTING.md
@ -105,7 +105,7 @@ Sprint retrospective
|
||||
|
||||
## Philosophy
|
||||
|
||||
We practise [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
|
||||
We practice [collective code ownership](http://www.extremeprogramming.org/rules/collective.html) rather than strong code ownership, which means that:
|
||||
|
||||
* developers can make contributions to other people's PRs (after checking in with them)
|
||||
* we avoid blocking because someone else isn't working, so we sometimes take over PRs from other developers
|
||||
@ -115,7 +115,7 @@ We believe in open source contributions as a learning experience – everyone is
|
||||
|
||||
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
|
||||
|
||||
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
|
||||
As a volunteer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
|
||||
|
||||
<!--
|
||||
## Open-Source Bounties
|
||||
@ -149,3 +149,106 @@ Our Open-Source bounty program is a work-in-progress. Based on our future
|
||||
experience we will make changes and improvements. So keep an eye on this
|
||||
contribution guide.
|
||||
-->
|
||||
|
||||
## Programming
|
||||
|
||||
### Localization
|
||||
|
||||
#### Quotation Marks
|
||||
|
||||
The following characters are different from the programming quotation mark:
|
||||
|
||||
`"` or `\"`
|
||||
|
||||
Please copy and paste the following quotes for the languages:
|
||||
|
||||
* de: „Dies ist ein Beispielsatz.“
|
||||
* en: “This is a sample sentence.”
|
||||
* See <https://grammar.collinsdictionary.com/easy-learning/when-do-you-use-quotation-marks-or-in-english>
|
||||
|
||||
## Docker – More Closely
|
||||
|
||||
### Apple M1 Platform
|
||||
|
||||
***Attention:** For using Docker commands in Apple M1 environments!*
|
||||
|
||||
#### Environment Variable For Apple M1 Platform
|
||||
|
||||
To set the Docker platform environment variable in your terminal tab, run:
|
||||
|
||||
```bash
|
||||
# set env variable for your shell
|
||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||
```
|
||||
|
||||
#### Docker Compose Override File For Apple M1 Platform
|
||||
|
||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
|
||||
# for development
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
||||
# only once: init admin user and create indexes and contraints in Neo4j database
|
||||
$ docker compose exec backend yarn prod:migrate init
|
||||
# clean db
|
||||
$ docker compose exec backend yarn db:reset
|
||||
# seed db
|
||||
$ docker compose exec backend yarn db:seed
|
||||
|
||||
# for production
|
||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
||||
# only once: init admin user and create indexes and contraints in Neo4j database
|
||||
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
||||
```
|
||||
|
||||
### Analyzing Docker Builds
|
||||
|
||||
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
|
||||
|
||||
The `dive build` command is exactly the right one to fulfill what we are looking for.
|
||||
We can use it just like the `docker build` command and get an analysis afterwards.
|
||||
|
||||
So, in our main folder, we use it in the following way:
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target <layer-name> -t "ocelotsocialnetwork/<app-name>:local-<layer-name>" --build-arg BBUILD_DATE="<build-date>" --build-arg BBUILD_VERSION="<build-version>" --build-arg BBUILD_COMMIT="<build-commit>" <app-folder-name-or-dot>/
|
||||
```
|
||||
|
||||
The build arguments are optional.
|
||||
|
||||
For the specific applications, we use them as follows.
|
||||
|
||||
#### Backend
|
||||
|
||||
##### Production For Backend
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target production -t "ocelotsocialnetwork/backend:local-production" backend/
|
||||
```
|
||||
|
||||
##### Development For Backend
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target development -t "ocelotsocialnetwork/backend:local-development" backend/
|
||||
```
|
||||
|
||||
#### Webapp
|
||||
|
||||
##### Production For Webapp
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target production -t "ocelotsocialnetwork/webapp:local-production" webapp/
|
||||
```
|
||||
|
||||
##### Development For Webapp
|
||||
|
||||
```bash
|
||||
# in main folder
|
||||
$ dive build --target development -t "ocelotsocialnetwork/webapp:local-development" webapp/
|
||||
```
|
||||
|
||||
@ -1 +1 @@
|
||||
v12.19.0
|
||||
v19.4.0
|
||||
@ -1,7 +1,7 @@
|
||||
##################################################################################
|
||||
# BASE (Is pushed to DockerHub for rebranding) ###################################
|
||||
##################################################################################
|
||||
FROM node:12.19.0-alpine3.10 as base
|
||||
FROM node:19.4.0-alpine3.17 as base
|
||||
|
||||
# ENVs
|
||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||
|
||||
@ -19,12 +19,19 @@ Wait a little until your backend is up and running at [http://localhost:4000/](h
|
||||
## Installation without Docker
|
||||
|
||||
For the local installation you need a recent version of
|
||||
[node](https://nodejs.org/en/) (>= `v10.12.0`). We are using
|
||||
`12.19.0` and therefore we recommend to use the same version
|
||||
[Node](https://nodejs.org/en/) (>= `v16.19.0`). We are using
|
||||
`v19.4.0` and therefore we recommend to use the same version
|
||||
([see](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4082)
|
||||
some known problems with more recent node versions). You can use the
|
||||
[node version manager](https://github.com/nvm-sh/nvm) to switch
|
||||
between different local node versions.
|
||||
[node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
||||
between different local Node versions:
|
||||
|
||||
```bash
|
||||
# install Node
|
||||
$ cd backend
|
||||
$ nvm install v19.4.0
|
||||
$ nvm use v19.4.0
|
||||
```
|
||||
|
||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||
|
||||
@ -32,6 +39,10 @@ Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||
# in main folder
|
||||
$ cd backend
|
||||
$ yarn install
|
||||
# or just
|
||||
$ yarn
|
||||
# or just later on to use version of ".nvmrc" file
|
||||
$ nvm use && yarn
|
||||
```
|
||||
|
||||
Copy Environment Variables:
|
||||
|
||||
@ -10,4 +10,5 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'text'],
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test).js?(x)'],
|
||||
setupFilesAfterEnv: ['<rootDir>/test/setup.js']
|
||||
}
|
||||
|
||||
@ -65,22 +65,22 @@
|
||||
"linkifyjs": "~2.1.8",
|
||||
"lodash": "~4.17.14",
|
||||
"merge-graphql-schemas": "^1.7.8",
|
||||
"metascraper": "^5.11.8",
|
||||
"metascraper-audio": "^5.14.26",
|
||||
"metascraper-author": "^5.14.22",
|
||||
"metascraper": "^5.33.5",
|
||||
"metascraper-audio": "^5.33.5",
|
||||
"metascraper-author": "^5.33.5",
|
||||
"metascraper-clearbit-logo": "^5.3.0",
|
||||
"metascraper-date": "^5.11.8",
|
||||
"metascraper-description": "^5.23.1",
|
||||
"metascraper-image": "^5.11.8",
|
||||
"metascraper-lang": "^5.23.1",
|
||||
"metascraper-date": "^5.33.5",
|
||||
"metascraper-description": "^5.33.5",
|
||||
"metascraper-image": "^5.33.5",
|
||||
"metascraper-lang": "^5.33.5",
|
||||
"metascraper-lang-detector": "^4.10.2",
|
||||
"metascraper-logo": "^5.14.26",
|
||||
"metascraper-publisher": "^5.23.0",
|
||||
"metascraper-soundcloud": "^5.23.0",
|
||||
"metascraper-title": "^5.11.8",
|
||||
"metascraper-url": "^5.14.26",
|
||||
"metascraper-video": "^5.11.8",
|
||||
"metascraper-youtube": "^5.23.0",
|
||||
"metascraper-logo": "^5.33.5",
|
||||
"metascraper-publisher": "^5.33.5",
|
||||
"metascraper-soundcloud": "^5.33.5",
|
||||
"metascraper-title": "^5.33.5",
|
||||
"metascraper-url": "^5.33.5",
|
||||
"metascraper-video": "^5.33.5",
|
||||
"metascraper-youtube": "^5.33.5",
|
||||
"migrate": "^1.7.0",
|
||||
"mime-types": "^2.1.26",
|
||||
"minimatch": "^3.0.4",
|
||||
@ -123,6 +123,8 @@
|
||||
"supertest": "~4.0.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"fs-capacitor": "6.0.0"
|
||||
"**/**/fs-capacitor":"^6.2.0",
|
||||
"**/graphql-upload": "^11.0.0",
|
||||
"nan": "2.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,14 @@ const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
const { inviteCode } = args
|
||||
const response = await resolve(root, args, context, resolveInfo)
|
||||
const { email, nonce } = response
|
||||
if (nonce) {
|
||||
// emails that already exist do not have a nonce
|
||||
if (inviteCode) {
|
||||
await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } }))
|
||||
} else {
|
||||
await sendMail(signupTemplate({ email, variables: { nonce } }))
|
||||
}
|
||||
}
|
||||
delete response.nonce
|
||||
return response
|
||||
}
|
||||
@ -30,7 +33,9 @@ const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo)
|
||||
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
|
||||
const response = await resolve(root, args, context, resolveInfo)
|
||||
const { email, nonce, name } = response
|
||||
if (nonce) {
|
||||
await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } }))
|
||||
}
|
||||
delete response.nonce
|
||||
return response
|
||||
}
|
||||
|
||||
7
backend/src/schema/resolvers/Upload.js
Normal file
7
backend/src/schema/resolvers/Upload.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { GraphQLUpload } from 'graphql-upload'
|
||||
|
||||
export default {
|
||||
// This maps the `Upload` scalar to the implementation provided
|
||||
// by the `graphql-upload` package.
|
||||
Upload: GraphQLUpload,
|
||||
}
|
||||
@ -40,7 +40,9 @@ export default {
|
||||
}
|
||||
|
||||
// check email does not belong to anybody
|
||||
await existingEmailAddress({ args, context })
|
||||
const existingEmail = await existingEmailAddress({ args, context })
|
||||
if (existingEmail && existingEmail.alreadyExistingEmail && existingEmail.user)
|
||||
return existingEmail.alreadyExistingEmail
|
||||
|
||||
const nonce = generateNonce()
|
||||
const {
|
||||
|
||||
@ -134,11 +134,17 @@ describe('AddEmailAddress', () => {
|
||||
})
|
||||
|
||||
describe('but if another user owns an `EmailAddress` already with that email', () => {
|
||||
it('throws UserInputError because of unique constraints', async () => {
|
||||
it('does not throw UserInputError', async () => {
|
||||
await Factory.build('user', {}, { email: 'new-email@example.org' })
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
data: { AddEmailAddress: null },
|
||||
errors: [{ message: 'A user account with this email already exists.' }],
|
||||
data: {
|
||||
AddEmailAddress: {
|
||||
createdAt: expect.any(String),
|
||||
verifiedAt: null,
|
||||
email: 'new-email@example.org',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -96,7 +96,7 @@ describe('Query', () => {
|
||||
description: null,
|
||||
html: null,
|
||||
image: null,
|
||||
lang: null,
|
||||
lang: 'false',
|
||||
publisher: null,
|
||||
sources: ['resource'],
|
||||
title: null,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { UserInputError } from 'apollo-server'
|
||||
|
||||
export default async function alreadyExistingMail({ args, context }) {
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
@ -20,9 +18,11 @@ export default async function alreadyExistingMail({ args, context }) {
|
||||
})
|
||||
})
|
||||
const [emailBelongsToUser] = await existingEmailAddressTxPromise
|
||||
const { alreadyExistingEmail, user } = emailBelongsToUser || {}
|
||||
/*
|
||||
const { alreadyExistingEmail, user } =
|
||||
if (user) throw new UserInputError('A user account with this email already exists.')
|
||||
return alreadyExistingEmail
|
||||
*/
|
||||
return emailBelongsToUser || {}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
@ -113,10 +113,11 @@ const sanitizeRelationshipType = (relationshipType) => {
|
||||
const localFileUpload = ({ createReadStream, uniqueFilename }) => {
|
||||
const destination = `/uploads/${uniqueFilename}`
|
||||
return new Promise((resolve, reject) =>
|
||||
createReadStream()
|
||||
.pipe(createWriteStream(`public${destination}`))
|
||||
createReadStream().pipe(
|
||||
createWriteStream(`public${destination}`)
|
||||
.on('finish', () => resolve(destination))
|
||||
.on('error', reject),
|
||||
.on('error', (error) => reject(error)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,12 @@ export default {
|
||||
args.nonce = generateNonce()
|
||||
args.email = normalizeEmail(args.email)
|
||||
let emailAddress = await existingEmailAddress({ args, context })
|
||||
if (emailAddress) return emailAddress
|
||||
/*
|
||||
if (emailAddress.user) {
|
||||
// what to do?
|
||||
}
|
||||
*/
|
||||
if (emailAddress.alreadyExistingEmail) return emailAddress.alreadyExistingEmail
|
||||
try {
|
||||
emailAddress = await neode.create('EmailAddress', args)
|
||||
return emailAddress.toJson()
|
||||
|
||||
@ -118,9 +118,9 @@ describe('Signup', () => {
|
||||
await emailAddress.relateTo(user, 'belongsTo')
|
||||
})
|
||||
|
||||
it('throws UserInputError error because of unique constraint violation', async () => {
|
||||
it('does not throw UserInputError error', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
errors: [{ message: 'A user account with this email already exists.' }],
|
||||
data: { Signup: { email: 'someuser@example.org' } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -12,6 +12,7 @@ import { RedisPubSub } from 'graphql-redis-subscriptions'
|
||||
import { PubSub } from 'graphql-subscriptions'
|
||||
import Redis from 'ioredis'
|
||||
import bodyParser from 'body-parser'
|
||||
import { graphqlUploadExpress } from 'graphql-upload'
|
||||
|
||||
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
||||
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
|
||||
@ -67,6 +68,7 @@ const createServer = (options) => {
|
||||
},
|
||||
},
|
||||
debug: !!CONFIG.DEBUG,
|
||||
uploads: false,
|
||||
tracing: !!CONFIG.DEBUG,
|
||||
formatError: (error) => {
|
||||
if (error.message === 'ERROR_VALIDATION') {
|
||||
@ -85,6 +87,7 @@ const createServer = (options) => {
|
||||
app.use(express.static('public'))
|
||||
app.use(bodyParser.json({ limit: '10mb' }))
|
||||
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }))
|
||||
app.use(graphqlUploadExpress())
|
||||
server.applyMiddleware({ app, path: '/' })
|
||||
const httpServer = http.createServer(app)
|
||||
server.installSubscriptionHandlers(httpServer)
|
||||
|
||||
8
backend/test/setup.js
Normal file
8
backend/test/setup.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Polyfill missing encoders in jsdom
|
||||
// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest
|
||||
import { TextEncoder, TextDecoder } from 'util'
|
||||
global.TextEncoder = TextEncoder
|
||||
global.TextDecoder = TextDecoder
|
||||
|
||||
// Metascraper takes longer nowadays, double time
|
||||
jest.setTimeout(10000)
|
||||
1336
backend/yarn.lock
1336
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
{
|
||||
"projectId": "qa7fe2",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"ignoreTestFiles": "*.js",
|
||||
"chromeWebSecurity": false,
|
||||
"baseUrl": "http://localhost:3000",
|
||||
|
||||
@ -13,29 +13,29 @@ Feature: User profile - list social media accounts
|
||||
When I navigate to page "/settings/my-social-media"
|
||||
Then I am on page "/settings/my-social-media"
|
||||
When I add a social media link
|
||||
Then I see a toaster with "Added social media"
|
||||
Then I see a toaster with status "success"
|
||||
And the new social media link shows up on the page
|
||||
|
||||
Scenario: Other users viewing my Social Media
|
||||
Given I have added a social media link
|
||||
Given I have added the social media link "https://freeradical.zone/peter-pan"
|
||||
When I navigate to page "/profile/peter-pan"
|
||||
Then they should be able to see my social media links
|
||||
|
||||
Scenario: Deleting Social Media
|
||||
When I navigate to page "/settings/my-social-media"
|
||||
Then I am on page "/settings/my-social-media"
|
||||
Given I have added a social media link
|
||||
When I delete a social media link
|
||||
Then I see a toaster with "Deleted social media"
|
||||
Given I have added the social media link "https://freeradical.zone/peter-pan"
|
||||
When I delete the social media link "https://freeradical.zone/peter-pan"
|
||||
Then I see a toaster with status "success"
|
||||
|
||||
Scenario: Editing Social Media
|
||||
When I navigate to page "/settings/my-social-media"
|
||||
Then I am on page "/settings/my-social-media"
|
||||
Given I have added a social media link
|
||||
Given I have added the social media link "https://freeradical.zone/peter-pan"
|
||||
When I start editing a social media link
|
||||
Then I can cancel editing
|
||||
When I start editing a social media link
|
||||
And I edit and save the link
|
||||
Then I see a toaster with "Added social media"
|
||||
Then I see a toaster with status "success"
|
||||
And the new url is displayed
|
||||
But the old url is not displayed
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
When('I add a social media link', () => {
|
||||
cy.get('button')
|
||||
.contains('Add link')
|
||||
cy.get('[data-test="add-save-button"]')
|
||||
.click()
|
||||
.get('#editSocialMedia')
|
||||
.type('https://freeradical.zone/peter-pan')
|
||||
.get('button')
|
||||
.contains('Save')
|
||||
.get('[data-test="add-save-button"]')
|
||||
.click()
|
||||
})
|
||||
@ -1,6 +0,0 @@
|
||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
When('I delete a social media link', () => {
|
||||
cy.get(".base-button[title='Delete']")
|
||||
.click()
|
||||
})
|
||||
@ -0,0 +1,12 @@
|
||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
When('I delete the social media link {string}', (link) => {
|
||||
cy.get('[data-test="delete-button"]')
|
||||
.click()
|
||||
cy.get('[data-test="confirm-modal"]')
|
||||
.should("be.visible")
|
||||
cy.get('[data-test="confirm-button"]')
|
||||
.click()
|
||||
cy.get('.ds-list-item-content > a')
|
||||
.contains(link).should('not.exist')
|
||||
})
|
||||
@ -4,7 +4,6 @@ When('I edit and save the link', () => {
|
||||
cy.get('input#editSocialMedia')
|
||||
.clear()
|
||||
.type('https://freeradical.zone/tinkerbell')
|
||||
.get('button')
|
||||
.contains('Save')
|
||||
.get('[data-test="add-save-button"]')
|
||||
.click()
|
||||
})
|
||||
@ -1,13 +0,0 @@
|
||||
import { Given } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
Given('I have added a social media link', () => {
|
||||
cy.visit('/settings/my-social-media')
|
||||
.get('button')
|
||||
.contains('Add link')
|
||||
.click()
|
||||
.get('#editSocialMedia')
|
||||
.type('https://freeradical.zone/peter-pan')
|
||||
.get('button')
|
||||
.contains('Save')
|
||||
.click()
|
||||
})
|
||||
@ -0,0 +1,13 @@
|
||||
import { Given } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
Given('I have added the social media link {string}', (link) => {
|
||||
cy.visit('/settings/my-social-media')
|
||||
.get('[data-test="add-save-button"]')
|
||||
.click()
|
||||
.get('#editSocialMedia')
|
||||
.type(link)
|
||||
.get('[data-test="add-save-button"]')
|
||||
.click()
|
||||
cy.get('.ds-list-item-content > a')
|
||||
.contains(link)
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
When('I start editing a social media link', () => {
|
||||
cy.get(".base-button[title='Edit']")
|
||||
cy.get('[data-test="edit-button"]')
|
||||
.click()
|
||||
})
|
||||
@ -1,8 +1,8 @@
|
||||
import { Then } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
Then('they should be able to see my social media links', () => {
|
||||
cy.get('.base-card')
|
||||
.contains('Where else can I find Peter Pan?')
|
||||
cy.get('[data-test="social-media-list-headline"]')
|
||||
.contains('Peter Pan')
|
||||
.get('a[href="https://freeradical.zone/peter-pan"]')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
@ -0,0 +1,9 @@
|
||||
import { Then } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
Then("I see a toaster with status {string}", (status) => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
cy.get(".iziToast.iziToast-color-green").should("be.visible");
|
||||
break;
|
||||
}
|
||||
})
|
||||
@ -48,7 +48,8 @@
|
||||
"slug": "^6.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"set-value": "^2.0.1"
|
||||
"set-value": "^2.0.1",
|
||||
"nan": "2.17.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
v16.19.0
|
||||
v19.4.0
|
||||
@ -1,7 +1,7 @@
|
||||
##################################################################################
|
||||
# BASE (Is pushed to DockerHub for rebranding) ###################################
|
||||
##################################################################################
|
||||
FROM node:16.19.0-alpine3.17 as base
|
||||
FROM node:19.4.0-alpine3.17 as base
|
||||
|
||||
# ENVs
|
||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
##################################################################################
|
||||
# BASE ###########################################################################
|
||||
##################################################################################
|
||||
FROM node:16.19.0-alpine3.17 as base
|
||||
FROM node:19.4.0-alpine3.17 as base
|
||||
|
||||
# ENVs
|
||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||
|
||||
@ -4,18 +4,32 @@
|
||||
|
||||
## Installation
|
||||
|
||||
For preparation we need Node and recommend to use [node version manager](https://github.com/nvm-sh/nvm) `nvm` to switch
|
||||
between different local Node versions:
|
||||
|
||||
```bash
|
||||
# install Node
|
||||
$ cd webapp
|
||||
$ nvm install v16.19.0
|
||||
$ nvm use v16.19.0
|
||||
```
|
||||
|
||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||
|
||||
```bash
|
||||
# install all dependencies
|
||||
$ cd webapp/
|
||||
$ cd webapp
|
||||
$ yarn install
|
||||
# or just
|
||||
$ yarn
|
||||
# or just later on to use version of ".nvmrc" file
|
||||
$ nvm use && yarn
|
||||
```
|
||||
|
||||
Copy:
|
||||
|
||||
```text
|
||||
# in webapp/
|
||||
# in webapp
|
||||
cp .env.template .env
|
||||
```
|
||||
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
@change="changeGroupType($event)"
|
||||
>
|
||||
<option v-for="groupType in groupTypeOptions" :key="groupType" :value="groupType">
|
||||
{{ $t(`group.types.${groupType}`) }}
|
||||
{{ $t(`group.typesOptions.${groupType}`) }}
|
||||
</option>
|
||||
</select>
|
||||
<ds-chip
|
||||
@ -149,21 +149,21 @@
|
||||
<ds-space margin-top="small" />
|
||||
|
||||
<!-- category -->
|
||||
<div v-if="categoriesActive">
|
||||
<ds-text class="select-label">
|
||||
{{ $t('group.categoriesTitle') }}
|
||||
</ds-text>
|
||||
|
||||
<categories-select
|
||||
v-if="categoriesActive"
|
||||
model="categoryIds"
|
||||
name="categoryIds"
|
||||
:existingCategoryIds="formData.categoryIds"
|
||||
/>
|
||||
<ds-chip
|
||||
v-if="categoriesActive"
|
||||
size="base"
|
||||
:color="errors && errors.categoryIds ? 'danger' : 'medium'"
|
||||
>
|
||||
<ds-chip size="base" :color="errors && errors.categoryIds ? 'danger' : 'medium'">
|
||||
{{ formData.categoryIds.length }} / 3
|
||||
<base-icon v-if="errors && errors.categoryIds" name="warning" />
|
||||
</ds-chip>
|
||||
|
||||
</div>
|
||||
<!-- submit -->
|
||||
<ds-space margin-top="large">
|
||||
<nuxt-link to="/groups">
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
:name="name"
|
||||
@close="close"
|
||||
/>
|
||||
<!-- "id", "type", and "name" props are only used for compatibility with the other modals -->
|
||||
<confirm-modal
|
||||
v-if="open === 'confirm'"
|
||||
:id="data.resource.id"
|
||||
@ -57,6 +58,7 @@ export default {
|
||||
open: 'modal/open',
|
||||
}),
|
||||
name() {
|
||||
// REFACTORING: This gets unneccesary if we use "modalData" in all modals by probaply replacing them all by "confirm-modal"
|
||||
if (!this.data || !this.data.resource) return ''
|
||||
const {
|
||||
resource: { name, title, author },
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel">
|
||||
<ds-modal :title="title" :is-open="isOpen" @cancel="cancel" data-test="confirm-modal">
|
||||
<transition name="ds-transition-fade">
|
||||
<ds-flex v-if="success" class="hc-modal-success" centered>
|
||||
<sweetalert-icon icon="success" />
|
||||
@ -15,6 +15,7 @@
|
||||
:danger="!modalData.buttons.confirm.danger"
|
||||
:icon="modalData.buttons.cancel.icon"
|
||||
@click="cancel"
|
||||
data-test="cancel-button"
|
||||
>
|
||||
{{ $t(modalData.buttons.cancel.textIdent) }}
|
||||
</base-button>
|
||||
@ -25,6 +26,7 @@
|
||||
:icon="modalData.buttons.confirm.icon"
|
||||
:loading="loading"
|
||||
@click="confirm"
|
||||
data-test="confirm-button"
|
||||
>
|
||||
{{ $t(modalData.buttons.confirm.textIdent) }}
|
||||
</base-button>
|
||||
@ -41,10 +43,10 @@ export default {
|
||||
SweetalertIcon,
|
||||
},
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
type: { type: String, required: true },
|
||||
name: { type: String, default: '' }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
type: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
modalData: { type: Object, required: true },
|
||||
id: { type: String, required: true },
|
||||
id: { type: String, required: true }, // only used for compatibility with the other modals in 'Modal.vue'
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -30,7 +30,10 @@
|
||||
v-for="category in post.categories"
|
||||
:key="category.id"
|
||||
v-tooltip="{
|
||||
content: $t(`contribution.category.description.${category.slug}`),
|
||||
content: `
|
||||
${$t(`contribution.category.name.${category.slug}`)}:
|
||||
${$t(`contribution.category.description.${category.slug}`)}
|
||||
`,
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
:icon="category.icon"
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<ds-space v-if="user.socialMedia && user.socialMedia.length" margin="large">
|
||||
<base-card class="social-media-bc">
|
||||
<ds-space margin="x-small">
|
||||
<ds-text tag="h5" color="soft">
|
||||
<ds-text tag="h5" color="soft" data-test="social-media-list-headline">
|
||||
{{ $t('profile.socialMedia') }} {{ userName | truncate(15) }}?
|
||||
</ds-text>
|
||||
<template>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import MySomethingList from './MySomethingList.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -9,12 +10,23 @@ describe('MySomethingList.vue', () => {
|
||||
let propsData
|
||||
let data
|
||||
let mocks
|
||||
let mutations
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
useFormData: { dummy: '' },
|
||||
useItems: [{ id: 'id', dummy: 'dummy' }],
|
||||
namePropertyKey: 'dummy',
|
||||
texts: {
|
||||
addButton: 'add-button',
|
||||
addNew: 'add-new-something',
|
||||
deleteModal: {
|
||||
titleIdent: 'delete-modal.title',
|
||||
messageIdent: 'delete-modal.message',
|
||||
confirm: { icon: 'trash', buttonTextIdent: 'delete-modal.confirm-button' },
|
||||
},
|
||||
edit: 'edit-something',
|
||||
},
|
||||
callbacks: { edit: jest.fn(), submit: jest.fn(), delete: jest.fn() },
|
||||
}
|
||||
data = () => {
|
||||
@ -30,6 +42,9 @@ describe('MySomethingList.vue', () => {
|
||||
success: jest.fn(),
|
||||
},
|
||||
}
|
||||
mutations = {
|
||||
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
@ -39,12 +54,16 @@ describe('MySomethingList.vue', () => {
|
||||
'list-item': '<div class="list-item"></div>',
|
||||
'edit-item': '<div class="edit-item"></div>',
|
||||
}
|
||||
const store = new Vuex.Store({
|
||||
mutations,
|
||||
})
|
||||
return mount(MySomethingList, {
|
||||
propsData,
|
||||
data,
|
||||
mocks,
|
||||
localVue,
|
||||
slots,
|
||||
store,
|
||||
})
|
||||
}
|
||||
|
||||
@ -114,13 +133,42 @@ describe('MySomethingList.vue', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('calls delete', async () => {
|
||||
it('calls delete by committing "modal/SET_OPEN"', async () => {
|
||||
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
||||
deleteButton.trigger('click')
|
||||
await Vue.nextTick()
|
||||
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
|
||||
expect(propsData.callbacks.delete).toHaveBeenCalledTimes(1)
|
||||
expect(propsData.callbacks.delete).toHaveBeenCalledWith(expect.any(Object), expectedItem)
|
||||
const expectedModalData = expect.objectContaining({
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: 'delete-modal.title',
|
||||
messageIdent: 'delete-modal.message',
|
||||
messageParams: {
|
||||
name: 'dummy',
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: 'trash',
|
||||
textIdent: 'delete-modal.confirm-button',
|
||||
callback: expect.any(Function),
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'actions.cancel',
|
||||
callback: expect.any(Function),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledTimes(1)
|
||||
expect(mutations['modal/SET_OPEN']).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expectedModalData,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -9,11 +9,7 @@
|
||||
<div v-if="isEditing">
|
||||
<ds-space margin="base">
|
||||
<ds-heading tag="h5">
|
||||
{{
|
||||
isCreation
|
||||
? $t('settings.social-media.addNewTitle')
|
||||
: $t('settings.social-media.editTitle', { name: editingItem[namePropertyKey] })
|
||||
}}
|
||||
{{ isCreation ? texts.addNew : texts.edit + ' — ' + editingItem[namePropertyKey] }}
|
||||
</ds-heading>
|
||||
</ds-space>
|
||||
<ds-space v-if="items" margin-top="base">
|
||||
@ -36,11 +32,11 @@
|
||||
data-test="edit-button"
|
||||
/>
|
||||
<base-button
|
||||
:title="$t('actions.delete')"
|
||||
icon="trash"
|
||||
circle
|
||||
ghost
|
||||
@click="handleDeleteItem(item)"
|
||||
:title="$t('actions.delete')"
|
||||
data-test="delete-button"
|
||||
/>
|
||||
</template>
|
||||
@ -58,7 +54,7 @@
|
||||
type="submit"
|
||||
data-test="add-save-button"
|
||||
>
|
||||
{{ isEditing ? $t('actions.save') : $t('settings.social-media.submit') }}
|
||||
{{ isEditing ? $t('actions.save') : texts.addButton }}
|
||||
</base-button>
|
||||
<base-button v-if="isEditing" id="cancel" danger @click="handleCancel()">
|
||||
{{ $t('actions.cancel') }}
|
||||
@ -69,27 +65,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'MySomethingList',
|
||||
props: {
|
||||
useFormData: {
|
||||
useFormData: { type: Object, default: () => ({}) },
|
||||
useFormSchema: { type: Object, default: () => ({}) },
|
||||
useItems: { type: Array, default: () => [] },
|
||||
defaultItem: { type: Object, default: () => ({}) },
|
||||
namePropertyKey: { type: String, required: true },
|
||||
texts: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
useFormSchema: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
useItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
defaultItem: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
namePropertyKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
callbacks: {
|
||||
@ -128,6 +115,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
commitModalData: 'modal/SET_OPEN',
|
||||
}),
|
||||
handleInput(data) {
|
||||
this.callbacks.handleInput(this, data)
|
||||
this.disabled = true
|
||||
@ -155,8 +145,42 @@ export default {
|
||||
this.editingItem = null
|
||||
this.disabled = true
|
||||
},
|
||||
async handleDeleteItem(item) {
|
||||
await this.callbacks.delete(this, item)
|
||||
handleDeleteItem(item) {
|
||||
this.openModal(item)
|
||||
},
|
||||
openModal(item) {
|
||||
this.commitModalData(this.modalData(item))
|
||||
},
|
||||
modalData(item) {
|
||||
return {
|
||||
name: 'confirm',
|
||||
data: {
|
||||
type: '',
|
||||
resource: { id: '' },
|
||||
modalData: {
|
||||
titleIdent: this.texts.deleteModal.titleIdent,
|
||||
messageIdent: this.texts.deleteModal.messageIdent,
|
||||
messageParams: {
|
||||
name: item[this.namePropertyKey],
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
danger: true,
|
||||
icon: this.texts.deleteModal.confirm.icon,
|
||||
textIdent: this.texts.deleteModal.confirm.buttonTextIdent,
|
||||
callback: () => {
|
||||
this.callbacks.delete(this, item)
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
icon: 'close',
|
||||
textIdent: 'actions.cancel',
|
||||
callback: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
40
webapp/graphql/SocialMedia.js
Normal file
40
webapp/graphql/SocialMedia.js
Normal file
@ -0,0 +1,40 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// ------ mutations
|
||||
|
||||
export const createSocialMediaMutation = () => {
|
||||
return gql`
|
||||
mutation ($url: String!) {
|
||||
CreateSocialMedia(url: $url) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const updateSocialMediaMutation = () => {
|
||||
return gql`
|
||||
mutation ($id: ID!, $url: String!) {
|
||||
UpdateSocialMedia(id: $id, url: $url) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const deleteSocialMediaMutation = () => {
|
||||
return gql`
|
||||
mutation ($id: ID!) {
|
||||
DeleteSocialMedia(id: $id) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
// ------ queries
|
||||
|
||||
// put the queries here
|
||||
@ -397,13 +397,13 @@
|
||||
},
|
||||
"group": {
|
||||
"actionRadii": {
|
||||
"continental": "Kontinentale Gruppe",
|
||||
"global": "Globale Gruppe",
|
||||
"interplanetary": "Interplanetare Gruppe",
|
||||
"national": "Nationale Gruppe",
|
||||
"regional": "Regionale Gruppe"
|
||||
"continental": "Kontinental",
|
||||
"global": "Global oder nur virtuell",
|
||||
"interplanetary": "Interplanetar",
|
||||
"national": "National",
|
||||
"regional": "Regional"
|
||||
},
|
||||
"actionRadius": "Aktionsradius",
|
||||
"actionRadius": "Aktionsradius der Gruppe",
|
||||
"addMemberToGroup": "Zur Gruppe hinzufügen",
|
||||
"addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!",
|
||||
"addUser": "Benutzer hinzufügen",
|
||||
@ -414,6 +414,7 @@
|
||||
"tooltip": "Gruppen anzeigen"
|
||||
},
|
||||
"categories": "Thema ::: Themen",
|
||||
"categoriesTitle": "Themen der Gruppe",
|
||||
"changeMemberRole": "Die Rolle wurde auf „{role}“ geändert!",
|
||||
"contentMenu": {
|
||||
"visitGroupPage": "Gruppe anzeigen"
|
||||
@ -475,11 +476,16 @@
|
||||
"usual": "Einfaches Mitglied"
|
||||
},
|
||||
"save": "Neue Gruppe anlegen",
|
||||
"type": "Gruppentyp",
|
||||
"type": "Öffentlichkeit der Gruppe",
|
||||
"types": {
|
||||
"closed": "Geschlossene Gruppe",
|
||||
"hidden": "Versteckte Gruppe",
|
||||
"public": "Öffentliche Gruppe"
|
||||
"closed": "Geschlossen",
|
||||
"hidden": "Geheim",
|
||||
"public": "Öffentlich"
|
||||
},
|
||||
"typesOptions": {
|
||||
"closed": "Geschlossen — Alle Beiträge nur für Gruppenmitglieder sichtbar",
|
||||
"hidden": "Geheim — Gruppe (auch der Name) komplett unsichtbar",
|
||||
"public": "Öffentlich — Gruppe und alle Beiträge für registrierte Nutzer sichtbar"
|
||||
},
|
||||
"update": "Änderung speichern",
|
||||
"updatedGroup": "Die Gruppendaten wurden geändert!"
|
||||
@ -937,9 +943,14 @@
|
||||
"name": "Sicherheit"
|
||||
},
|
||||
"social-media": {
|
||||
"addNewTitle": "Neuen Link hinzufügen",
|
||||
"editTitle": "Link \"{name}\" ändern",
|
||||
"name": "Soziale Netzwerke",
|
||||
"add-new-link": "Neuen Link hinzufügen",
|
||||
"delete-modal": {
|
||||
"confirm-button": "Löschen",
|
||||
"message": "Lösche „{name}“.",
|
||||
"title": "Möchtest du wirklich deinen Link löschen?"
|
||||
},
|
||||
"edit-link": "Ändere den Link",
|
||||
"name": "Soziale Medien",
|
||||
"placeholder": "Deine Webadresse des Sozialen Netzwerkes",
|
||||
"requireUnique": "Dieser Link existiert bereits",
|
||||
"submit": "Link hinzufügen",
|
||||
|
||||
@ -397,13 +397,13 @@
|
||||
},
|
||||
"group": {
|
||||
"actionRadii": {
|
||||
"continental": "Continental Group",
|
||||
"global": "Global Group",
|
||||
"interplanetary": "Interplanetary Group",
|
||||
"national": "National Group",
|
||||
"regional": "Regional Group"
|
||||
"continental": "Continental",
|
||||
"global": "Global or only virtual",
|
||||
"interplanetary": "Interplanetary",
|
||||
"national": "National",
|
||||
"regional": "Regional"
|
||||
},
|
||||
"actionRadius": "Action radius",
|
||||
"actionRadius": "Action radius of the group",
|
||||
"addMemberToGroup": "Add to group",
|
||||
"addMemberToGroupSuccess": "“{name}” was added to the group with the role “{role}”!",
|
||||
"addUser": "Add User",
|
||||
@ -414,6 +414,7 @@
|
||||
"tooltip": "Show groups"
|
||||
},
|
||||
"categories": "Topic ::: Topics",
|
||||
"categoriesTitle": "Topics of the group",
|
||||
"changeMemberRole": "The role has been changed to “{role}”!",
|
||||
"contentMenu": {
|
||||
"visitGroupPage": "Show group"
|
||||
@ -475,11 +476,16 @@
|
||||
"usual": "Simple Member"
|
||||
},
|
||||
"save": "Create new group",
|
||||
"type": "Group type",
|
||||
"type": "Visibility of the group",
|
||||
"types": {
|
||||
"closed": "Closed Group",
|
||||
"hidden": "Hidden Group",
|
||||
"public": "Public Group"
|
||||
"closed": "Closed",
|
||||
"hidden": "Secret",
|
||||
"public": "Public"
|
||||
},
|
||||
"typesOptions": {
|
||||
"closed": "Closed — All posts only visible to the group's members",
|
||||
"hidden": "Secret — Group (including the name) is completely invisible",
|
||||
"public": "Public — Group and all posts are visible for all registered users"
|
||||
},
|
||||
"update": "Save change",
|
||||
"updatedGroup": "The group data has been changed."
|
||||
@ -937,8 +943,13 @@
|
||||
"name": "Security"
|
||||
},
|
||||
"social-media": {
|
||||
"addNewTitle": "Add new link",
|
||||
"editTitle": "Edit link \"{name}\"",
|
||||
"add-new-link": "Add new link",
|
||||
"delete-modal": {
|
||||
"confirm-button": "Delete",
|
||||
"message": "Delete “{name}”.",
|
||||
"title": "Do you really want to delete your link?"
|
||||
},
|
||||
"edit-link": "Edit link",
|
||||
"name": "Social media",
|
||||
"placeholder": "Your social media url",
|
||||
"requireUnique": "You added this url already",
|
||||
|
||||
@ -8,9 +8,9 @@
|
||||
"private": false,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "yarn run nuxt -c nuxt.config.maintenance.js",
|
||||
"build": "yarn run nuxt build -c nuxt.config.maintenance.js",
|
||||
"start": "yarn run nuxt start -c nuxt.config.maintenance.js",
|
||||
"generate": "yarn run nuxt generate -c nuxt.config.maintenance.js"
|
||||
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt -c nuxt.config.maintenance.js",
|
||||
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt build -c nuxt.config.maintenance.js",
|
||||
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt start -c nuxt.config.maintenance.js",
|
||||
"generate": "cross-env NODE_OPTIONS=--openssl-legacy-provider yarn run nuxt generate -c nuxt.config.maintenance.js"
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,12 @@
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt",
|
||||
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn run dev",
|
||||
"storybook": "start-storybook -p 3002 -s ./static -c storybook/",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate:maintenance": "nuxt generate -c nuxt.config.maintenance.js",
|
||||
"generate": "nuxt generate",
|
||||
"storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 3002 -s ./static -c storybook/",
|
||||
"build": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt build",
|
||||
"start": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt start",
|
||||
"generate": "cross-env NODE_OPTIONS=--openssl-legacy-provider nuxt generate",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"locales": "../scripts/translations/missing-keys.sh && ../scripts/translations/sort.sh",
|
||||
"precommit": "yarn lint",
|
||||
@ -112,5 +111,8 @@
|
||||
"vue-jest": "~3.0.5",
|
||||
"vue-svg-loader": "~0.16.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"resolutions": {
|
||||
"nan": "2.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ describe('my-social-media.vue', () => {
|
||||
let wrapper
|
||||
let mocks
|
||||
let getters
|
||||
let mutations
|
||||
const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
|
||||
const newSocialMediaUrl = 'https://twitter.com/mattwr18'
|
||||
const faviconUrl = 'https://freeradical.zone/favicon.ico'
|
||||
@ -30,6 +31,9 @@ describe('my-social-media.vue', () => {
|
||||
return {}
|
||||
},
|
||||
}
|
||||
mutations = {
|
||||
'modal/SET_OPEN': jest.fn().mockResolvedValueOnce(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
@ -37,6 +41,7 @@ describe('my-social-media.vue', () => {
|
||||
const Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
mutations,
|
||||
})
|
||||
return mount(MySocialMedia, { store, mocks, localVue })
|
||||
}
|
||||
@ -145,11 +150,14 @@ describe('my-social-media.vue', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleting social media link', () => {
|
||||
// TODO: confirm deletion modal is not present
|
||||
describe.skip('deleting social media link', () => {
|
||||
beforeEach(async () => {
|
||||
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
|
||||
deleteButton.trigger('click')
|
||||
await Vue.nextTick()
|
||||
// wrapper.find('button.cancel').trigger('click')
|
||||
// await Vue.nextTick()
|
||||
})
|
||||
|
||||
it('sends the link id to the backend', () => {
|
||||
|
||||
@ -7,13 +7,8 @@
|
||||
:useItems="socialMediaLinks"
|
||||
:defaultItem="{ url: '' }"
|
||||
:namePropertyKey="'url'"
|
||||
:callbacks="{
|
||||
handleInput: () => {},
|
||||
handleInputValid,
|
||||
edit: callbackEditSocialMedia,
|
||||
submit: handleSubmitSocialMedia,
|
||||
delete: callbackDeleteSocialMedia,
|
||||
}"
|
||||
:texts="mySomethingListTexts"
|
||||
:callbacks="mySomethingListCallbacks"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<social-media-list-item :item="item" />
|
||||
@ -33,7 +28,11 @@
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import unionBy from 'lodash/unionBy'
|
||||
import gql from 'graphql-tag'
|
||||
import {
|
||||
createSocialMediaMutation,
|
||||
updateSocialMediaMutation,
|
||||
deleteSocialMediaMutation,
|
||||
} from '~/graphql/SocialMedia.js'
|
||||
import MySomethingList from '~/components/_new/features/MySomethingList/MySomethingList.vue'
|
||||
import SocialMediaListItem from '~/components/_new/features/SocialMedia/SocialMediaListItem.vue'
|
||||
|
||||
@ -77,6 +76,30 @@ export default {
|
||||
return { id, url, favicon }
|
||||
})
|
||||
},
|
||||
mySomethingListTexts() {
|
||||
return {
|
||||
addButton: this.$t('settings.social-media.submit'),
|
||||
addNew: this.$t('settings.social-media.add-new-link'),
|
||||
deleteModal: {
|
||||
titleIdent: 'settings.social-media.delete-modal.title',
|
||||
messageIdent: 'settings.social-media.delete-modal.message',
|
||||
confirm: {
|
||||
icon: 'trash',
|
||||
buttonTextIdent: 'settings.social-media.delete-modal.confirm-button',
|
||||
},
|
||||
},
|
||||
edit: this.$t('settings.social-media.edit-link'),
|
||||
}
|
||||
},
|
||||
mySomethingListCallbacks() {
|
||||
return {
|
||||
handleInput: () => {},
|
||||
handleInputValid: this.handleInputValid,
|
||||
edit: this.callbackEditSocialMedia,
|
||||
submit: this.handleSubmitSocialMedia,
|
||||
delete: this.callbackDeleteSocialMedia,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
@ -93,7 +116,7 @@ export default {
|
||||
thisList.formData.socialMediaUrl = link.url
|
||||
// try to set focus on link edit field
|
||||
// thisList.$refs.socialMediaUrl.$el.focus()
|
||||
// !!! Check for existenz
|
||||
// !!! check for existence
|
||||
// this.$scopedSlots.default()[0].context.$refs
|
||||
// thisList.$scopedSlots['edit-item']()[0].$el.focus()
|
||||
// console.log(thisList.$scopedSlots['edit-item']()[0].context.$refs)
|
||||
@ -111,25 +134,11 @@ export default {
|
||||
|
||||
let mutation, variables, successMessage
|
||||
if (isCreation) {
|
||||
mutation = gql`
|
||||
mutation ($url: String!) {
|
||||
CreateSocialMedia(url: $url) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
mutation = createSocialMediaMutation()
|
||||
variables = { url: item.url }
|
||||
successMessage = thisList.$t('settings.social-media.successAdd')
|
||||
} else {
|
||||
mutation = gql`
|
||||
mutation ($id: ID!, $url: String!) {
|
||||
UpdateSocialMedia(id: $id, url: $url) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
mutation = updateSocialMediaMutation()
|
||||
variables = { id: item.id, url: item.url }
|
||||
successMessage = thisList.$t('settings.data.success')
|
||||
}
|
||||
@ -159,14 +168,7 @@ export default {
|
||||
async callbackDeleteSocialMedia(thisList, item) {
|
||||
try {
|
||||
await thisList.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($id: ID!) {
|
||||
DeleteSocialMedia(id: $id) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`,
|
||||
mutation: deleteSocialMediaMutation(),
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
|
||||
@ -15245,10 +15245,10 @@ mute-stream@0.0.8:
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
|
||||
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
|
||||
|
||||
nan@^2.12.1:
|
||||
version "2.13.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
||||
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
||||
nan@2.17.0, nan@^2.12.1:
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||
|
||||
nanoid@^3.1.23:
|
||||
version "3.1.23"
|
||||
|
||||
@ -4422,10 +4422,10 @@ mz@^2.4.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nan@^2.9.2:
|
||||
version "2.13.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd"
|
||||
integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==
|
||||
nan@2.17.0, nan@^2.9.2:
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user