diff --git a/admin/jest.config.js b/admin/jest.config.js index 253c905de..a0c8ae412 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -9,7 +9,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 96, + lines: 95, }, }, moduleFileExtensions: [ @@ -31,6 +31,9 @@ module.exports = { setupFiles: ['/test/testSetup.js', 'jest-canvas-mock'], testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], - transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], + transformIgnorePatterns: [ + '/node_modules/(?!vee-validate/dist/rules)', + '/node_modules/(?!@babel)', + ], testEnvironment: 'jest-environment-jsdom-sixteen', // why this is still needed? should not be needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen } diff --git a/admin/package.json b/admin/package.json index ca3205351..97d4f6fda 100644 --- a/admin/package.json +++ b/admin/package.json @@ -16,6 +16,7 @@ "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "test": "cross-env TZ=UTC jest", + "test:debug": "node --inspect-brk node_modules/.bin/vue-cli-service test:unit --no-cache --watch --runInBand", "locales": "scripts/sort.sh" }, "dependencies": { diff --git a/admin/src/components/Fedaration/FederationVisualizeItem.vue b/admin/src/components/Fedaration/FederationVisualizeItem.vue deleted file mode 100644 index a947387f4..000000000 --- a/admin/src/components/Fedaration/FederationVisualizeItem.vue +++ /dev/null @@ -1,63 +0,0 @@ - - diff --git a/admin/src/components/Fedaration/FederationVisualizeItem.spec.js b/admin/src/components/Federation/CommunityVisualizeItem.spec.js similarity index 53% rename from admin/src/components/Fedaration/FederationVisualizeItem.spec.js rename to admin/src/components/Federation/CommunityVisualizeItem.spec.js index 6058cc6f4..2c5065ba0 100644 --- a/admin/src/components/Fedaration/FederationVisualizeItem.spec.js +++ b/admin/src/components/Federation/CommunityVisualizeItem.spec.js @@ -1,36 +1,83 @@ import { mount } from '@vue/test-utils' -import FederationVisualizeItem from './FederationVisualizeItem.vue' +import Vuex from 'vuex' +import CommunityVisualizeItem from './CommunityVisualizeItem.vue' const localVue = global.localVue +localVue.use(Vuex) const today = new Date() const createdDate = new Date() createdDate.setDate(createdDate.getDate() - 3) +// Mock für den Vuex-Store +const store = new Vuex.Store({ + state: { + moderator: { + roles: ['ADMIN'], + }, + }, +}) + let propsData = { item: { - id: 7590, + id: 1, foreign: false, - publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7', - url: 'http://localhost/api/2_0', - lastAnnouncedAt: createdDate, - verifiedAt: today, - lastErrorAt: null, + url: 'http://localhost/api/', + publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2', + communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21', + authenticatedAt: null, + name: 'Gradido Test', + description: 'Gradido Community zum testen', + gmsApiKey: '', + creationDate: createdDate, createdAt: createdDate, - updatedAt: null, + updatedAt: createdDate, + federatedCommunities: [ + { + id: 2046, + apiVersion: '2_0', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: createdDate, + verifiedAt: today, + lastErrorAt: null, + createdAt: createdDate, + updatedAt: null, + }, + { + id: 2045, + apiVersion: '1_1', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: null, + verifiedAt: null, + lastErrorAt: null, + createdAt: '2024-01-16T10:08:21.550Z', + updatedAt: null, + }, + { + id: 2044, + apiVersion: '1_0', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: null, + verifiedAt: null, + lastErrorAt: null, + createdAt: '2024-01-16T10:08:21.544Z', + updatedAt: null, + }, + ], }, } const mocks = { + $t: (key) => key, $i18n: { locale: 'en', }, } -describe('FederationVisualizeItem', () => { +describe('CommunityVisualizeItem', () => { let wrapper const Wrapper = () => { - return mount(FederationVisualizeItem, { localVue, mocks, propsData }) + return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store }) } describe('mount', () => { @@ -39,19 +86,35 @@ describe('FederationVisualizeItem', () => { }) it('renders the component', () => { - expect(wrapper.find('div.federation-visualize-item').exists()).toBe(true) + expect(wrapper.exists()).toBe(true) + expect(wrapper.find('div.community-visualize-item').exists()).toBe(true) + expect(wrapper.find('.details').exists()).toBe(false) + }) + + it('toggles details on click', async () => { + // Click the row to toggle details + await wrapper.find('.row').trigger('click') + + // Assert that details are now open + expect(wrapper.find('.details').exists()).toBe(true) + + // Click the row again to toggle details back + await wrapper.find('.row').trigger('click') + + // Assert that details are now closed + expect(wrapper.find('.details').exists()).toBe(false) }) describe('rendering item properties', () => { it('has the url', () => { - expect(wrapper.find('.row > div:nth-child(2) > div').text()).toBe( - 'http://localhost/api/2_0', + expect(wrapper.find('.row > div:nth-child(2) > div > a').text()).toBe( + 'http://localhost/api/', ) }) it('has the public key', () => { expect(wrapper.find('.row > div:nth-child(2) > small').text()).toContain( - 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7'.substring(0, 26), + '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2'.substring(0, 26), ) }) @@ -65,33 +128,6 @@ describe('FederationVisualizeItem', () => { }) }) - describe('not verified item', () => { - beforeEach(() => { - propsData = { - item: { - id: 7590, - foreign: false, - publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7', - url: 'http://localhost/api/2_0', - lastAnnouncedAt: createdDate, - verifiedAt: null, - lastErrorAt: null, - createdAt: createdDate, - updatedAt: null, - }, - } - wrapper = Wrapper() - }) - - it('has the x-circle icon', () => { - expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true) - }) - - it('has the text variant "danger"', () => { - expect(wrapper.find('.text-danger').exists()).toBe(true) - }) - }) - // describe('with different locales (de, en, fr, es, nl)', () => { describe('lastAnnouncedAt', () => { it('computes the time string for different locales (de, en, fr, es, nl)', () => { @@ -155,6 +191,30 @@ describe('FederationVisualizeItem', () => { expect(wrapper.vm.createdAt).toBe('3 dagen geleden') }) + describe('not verified item', () => { + beforeEach(() => { + propsData = { + item: { + id: 7590, + foreign: false, + publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7', + url: 'http://localhost/api/', + createdAt: createdDate, + updatedAt: null, + }, + } + wrapper = Wrapper() + }) + + it('has the x-circle icon', () => { + expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true) + }) + + it('has the text variant "danger"', () => { + expect(wrapper.find('.text-danger').exists()).toBe(true) + }) + }) + describe('createdAt == null', () => { beforeEach(() => { propsData = { @@ -163,9 +223,9 @@ describe('FederationVisualizeItem', () => { foreign: false, publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7', url: 'http://localhost/api/2_0', - lastAnnouncedAt: createdDate, - verifiedAt: null, - lastErrorAt: null, + communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21', + authenticatedAt: null, + creationDate: null, createdAt: null, updatedAt: null, }, diff --git a/admin/src/components/Federation/CommunityVisualizeItem.vue b/admin/src/components/Federation/CommunityVisualizeItem.vue new file mode 100644 index 000000000..24e6b3712 --- /dev/null +++ b/admin/src/components/Federation/CommunityVisualizeItem.vue @@ -0,0 +1,160 @@ + + diff --git a/admin/src/components/Federation/FederationVisualizeItem.vue b/admin/src/components/Federation/FederationVisualizeItem.vue new file mode 100644 index 000000000..9172f9a94 --- /dev/null +++ b/admin/src/components/Federation/FederationVisualizeItem.vue @@ -0,0 +1,47 @@ + + diff --git a/admin/src/components/input/EditableLabel.spec.js b/admin/src/components/input/EditableLabel.spec.js new file mode 100644 index 000000000..0b2462b30 --- /dev/null +++ b/admin/src/components/input/EditableLabel.spec.js @@ -0,0 +1,83 @@ +// Test written by ChatGPT 3.5 +import { mount } from '@vue/test-utils' +import EditableLabel from './EditableLabel.vue' + +const localVue = global.localVue +const value = 'test label value' + +describe('EditableLabel', () => { + let wrapper + + const createWrapper = (propsData) => { + return mount(EditableLabel, { + localVue, + propsData, + }) + } + + it('renders the label when not editing', () => { + wrapper = createWrapper({ value, allowEdit: true }) + + expect(wrapper.find('label').text()).toBe(value) + }) + + it('renders the input when editing', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + await wrapper.find('button').trigger('click') + + expect(wrapper.find('input').exists()).toBe(true) + }) + + it('emits save event when clicking save button', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + await wrapper.find('button').trigger('click') + await wrapper.setData({ inputValue: 'New Value' }) + await wrapper.find('button').trigger('click') + + expect(wrapper.emitted().save).toBeTruthy() + expect(wrapper.emitted().save[0][0]).toBe('New Value') + }) + + it('disables save button when value is not changed', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + await wrapper.find('button').trigger('click') + + expect(wrapper.find('button').attributes('disabled')).toBe('disabled') + }) + + it('enables save button when value is changed', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + await wrapper.find('button').trigger('click') + await wrapper.setData({ inputValue: 'New Value' }) + + expect(wrapper.find('button').attributes('disabled')).toBeFalsy() + }) + + it('updates originalValue when saving changes', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + await wrapper.find('button').trigger('click') + await wrapper.setData({ inputValue: 'New Value' }) + await wrapper.find('button').trigger('click') + + expect(wrapper.vm.originalValue).toBe('New Value') + }) + + it('changes variant to success when editing', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + await wrapper.find('button').trigger('click') + + expect(wrapper.vm.variant).toBe('success') + }) + + it('changes variant to prime when not editing', async () => { + wrapper = createWrapper({ value, allowEdit: true }) + + expect(wrapper.vm.variant).toBe('prime') + }) +}) diff --git a/admin/src/components/input/EditableLabel.vue b/admin/src/components/input/EditableLabel.vue new file mode 100644 index 000000000..674fcd3e7 --- /dev/null +++ b/admin/src/components/input/EditableLabel.vue @@ -0,0 +1,64 @@ + + + diff --git a/admin/src/graphql/allCommunities.js b/admin/src/graphql/allCommunities.js new file mode 100644 index 000000000..6379086c7 --- /dev/null +++ b/admin/src/graphql/allCommunities.js @@ -0,0 +1,29 @@ +import gql from 'graphql-tag' + +export const allCommunities = gql` + query { + allCommunities { + foreign + url + publicKey + uuid + authenticatedAt + name + description + gmsApiKey + creationDate + createdAt + updatedAt + federatedCommunities { + id + apiVersion + endPoint + lastAnnouncedAt + verifiedAt + lastErrorAt + createdAt + updatedAt + } + } + } +` diff --git a/admin/src/graphql/getCommunities.js b/admin/src/graphql/getCommunities.js deleted file mode 100644 index ccf894f6b..000000000 --- a/admin/src/graphql/getCommunities.js +++ /dev/null @@ -1,17 +0,0 @@ -import gql from 'graphql-tag' - -export const getCommunities = gql` - query { - getCommunities { - id - foreign - publicKey - url - lastAnnouncedAt - verifiedAt - lastErrorAt - createdAt - updatedAt - } - } -` diff --git a/admin/src/graphql/updateHomeCommunity.js b/admin/src/graphql/updateHomeCommunity.js new file mode 100644 index 000000000..a43d6edd2 --- /dev/null +++ b/admin/src/graphql/updateHomeCommunity.js @@ -0,0 +1,9 @@ +import gql from 'graphql-tag' + +export const updateHomeCommunity = gql` + mutation ($uuid: String!, $gmsApiKey: String!) { + updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) { + id + } + } +` diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json index 7cc0affac..9f808f70c 100644 --- a/admin/src/locales/de.json +++ b/admin/src/locales/de.json @@ -73,11 +73,20 @@ "error": "Fehler", "expired": "abgelaufen", "federation": { + "apiVersion": "API Version", + "authenticatedAt": "Verifiziert am:", + "communityUuid": "Community UUID:", "createdAt": "Erstellt am", + "gmsApiKey": "GMS API Key:", + "toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!", "gradidoInstances": "Gradido Instanzen", "lastAnnouncedAt": "letzte Bekanntgabe", + "lastErrorAt": "Letzer Fehler am", + "name": "Name", + "publicKey": "PublicKey:", "url": "Url", - "verified": "Verifiziert" + "verified": "Verifiziert", + "verifiedAt": "Verifiziert am" }, "firstname": "Vorname", "footer": { diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json index 084e2104f..b1f020921 100644 --- a/admin/src/locales/en.json +++ b/admin/src/locales/en.json @@ -73,11 +73,20 @@ "error": "Error", "expired": "expired", "federation": { + "apiVersion": "API Version", + "authenticatedAt": "verified at:", + "communityUuid": "Community UUID:", "createdAt": "Created At ", + "gmsApiKey": "GMS API Key:", + "toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!", "gradidoInstances": "Gradido Instances", "lastAnnouncedAt": "Last Announced", + "lastErrorAt": "last error at", + "name": "Name", + "publicKey": "PublicKey:", "url": "Url", - "verified": "Verified" + "verified": "Verified", + "verifiedAt": "Verified at" }, "firstname": "Firstname", "footer": { diff --git a/admin/src/pages/FederationVisualize.spec.js b/admin/src/pages/FederationVisualize.spec.js index 67d6dffde..a089ef8fe 100644 --- a/admin/src/pages/FederationVisualize.spec.js +++ b/admin/src/pages/FederationVisualize.spec.js @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils' import FederationVisualize from './FederationVisualize' import VueApollo from 'vue-apollo' import { createMockClient } from 'mock-apollo-client' -import { getCommunities } from '@/graphql/getCommunities' +import { allCommunities } from '@/graphql/allCommunities' import { toastErrorSpy } from '../../test/testSetup' const mockClient = createMockClient() @@ -25,42 +25,54 @@ const mocks = { const defaultData = () => { return { - getCommunities: [ + allCommunities: [ { - id: 1776, - foreign: true, - publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527', - url: 'http://localhost/api/2_0', - lastAnnouncedAt: '2023-04-07T12:27:24.037Z', - verifiedAt: null, - lastErrorAt: null, - createdAt: '2023-04-07T11:45:06.254Z', - updatedAt: null, - __typename: 'Community', - }, - { - id: 1775, - foreign: true, - publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527', - url: 'http://localhost/api/1_1', - lastAnnouncedAt: '2023-04-07T12:27:24.023Z', - verifiedAt: null, - lastErrorAt: null, - createdAt: '2023-04-07T11:45:06.234Z', - updatedAt: null, - __typename: 'Community', - }, - { - id: 1774, - foreign: true, - publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527', - url: 'http://localhost/api/1_0', - lastAnnouncedAt: '2023-04-07T12:27:24.009Z', - verifiedAt: null, - lastErrorAt: null, - createdAt: '2023-04-07T11:45:06.218Z', - updatedAt: null, - __typename: 'Community', + id: 1, + foreign: false, + url: 'http://localhost/api/', + publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2', + communityUuid: '5ab0befd-b150-4f31-a631-7f3637e47b21', + authenticatedAt: null, + name: 'Gradido Test', + description: 'Gradido Community zum testen', + gmsApiKey: '', + creationDate: '2024-01-09T15:56:40.592Z', + createdAt: '2024-01-09T15:56:40.595Z', + updatedAt: '2024-01-16T11:17:15.000Z', + federatedCommunities: [ + { + id: 2046, + apiVersion: '2_0', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: null, + verifiedAt: null, + lastErrorAt: null, + createdAt: '2024-01-16T10:08:21.544Z', + updatedAt: null, + }, + { + id: 2045, + apiVersion: '1_1', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: null, + verifiedAt: null, + lastErrorAt: null, + createdAt: '2024-01-16T10:08:21.550Z', + updatedAt: null, + __typename: 'FederatedCommunity', + }, + { + id: 2044, + apiVersion: '1_0', + endPoint: 'http://localhost/api/', + lastAnnouncedAt: null, + verifiedAt: null, + lastErrorAt: null, + createdAt: '2024-01-16T10:08:21.544Z', + updatedAt: null, + __typename: 'FederatedCommunity', + }, + ], }, ], } @@ -68,11 +80,11 @@ const defaultData = () => { describe('FederationVisualize', () => { let wrapper - const getCommunitiesMock = jest.fn() + const allCommunitiesMock = jest.fn() mockClient.setRequestHandler( - getCommunities, - getCommunitiesMock + allCommunities, + allCommunitiesMock .mockRejectedValueOnce({ message: 'Ouch!' }) .mockResolvedValue({ data: defaultData() }), ) @@ -95,7 +107,7 @@ describe('FederationVisualize', () => { describe('sever success', () => { it('sends query to Apollo when created', () => { - expect(getCommunitiesMock).toBeCalled() + expect(allCommunitiesMock).toBeCalled() }) it('has a DIV element with the class "federation-visualize"', () => { @@ -106,8 +118,8 @@ describe('FederationVisualize', () => { expect(wrapper.find('[data-test="federation-communities-refresh-btn"]').exists()).toBe(true) }) - it('renders 3 community list items', () => { - expect(wrapper.findAll('.list-group-item').length).toBe(3) + it('renders 1 community list item', () => { + expect(wrapper.findAll('.list-group-item').length).toBe(1) }) describe('cklicking the refresh button', () => { @@ -117,7 +129,7 @@ describe('FederationVisualize', () => { }) it('calls the API', async () => { - expect(getCommunitiesMock).toBeCalled() + expect(allCommunitiesMock).toBeCalled() }) }) }) diff --git a/admin/src/pages/FederationVisualize.vue b/admin/src/pages/FederationVisualize.vue index 383cf064d..5736b1a0e 100644 --- a/admin/src/pages/FederationVisualize.vue +++ b/admin/src/pages/FederationVisualize.vue @@ -7,7 +7,7 @@ icon="arrow-clockwise" font-scale="2" :animation="animation" - @click="$apollo.queries.GetCommunities.refresh()" + @click="$apollo.queries.allCommunities.refresh()" data-test="federation-communities-refresh-btn" > @@ -16,28 +16,29 @@ {{ $t('federation.verified') }} {{ $t('federation.url') }} + {{ $t('federation.name') }} {{ $t('federation.lastAnnouncedAt') }} {{ $t('federation.createdAt') }} - + - diff --git a/frontend/src/components/UserSettings/UserGMSLocation.vue b/frontend/src/components/UserSettings/UserGMSLocation.vue new file mode 100644 index 000000000..0d7fe9ab7 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSLocation.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/components/UserSettings/UserGMSLocationFormat.spec.js b/frontend/src/components/UserSettings/UserGMSLocationFormat.spec.js new file mode 100644 index 000000000..9ec235d95 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSLocationFormat.spec.js @@ -0,0 +1,79 @@ +import { mount } from '@vue/test-utils' +import UserGMSLocationFormat from './UserGMSLocationFormat.vue' +import { toastErrorSpy } from '@test/testSetup' + +const mockAPIcall = jest.fn() + +const storeCommitMock = jest.fn() + +const localVue = global.localVue + +describe('UserGMSLocationFormat', () => { + let wrapper + beforeEach(() => { + wrapper = mount(UserGMSLocationFormat, { + mocks: { + $t: (key) => key, // Mocking the translation function + $store: { + state: { + gmsPublishLocation: null, + }, + commit: storeCommitMock, + }, + $apollo: { + mutate: mockAPIcall, + }, + }, + localVue, + propsData: { + selectedOption: 'GMS_LOCATION_TYPE_RANDOM', + }, + }) + }) + + afterEach(() => { + wrapper.destroy() + }) + + it('renders the correct dropdown options', () => { + const dropdownItems = wrapper.findAll('.dropdown-item') + expect(dropdownItems.length).toBe(3) + + const labels = dropdownItems.wrappers.map((item) => item.text()) + expect(labels).toEqual([ + 'settings.GMS.publish-location.exact', + 'settings.GMS.publish-location.approximate', + 'settings.GMS.publish-location.random', + ]) + }) + + it('updates selected option on click', async () => { + const dropdownItem = wrapper.findAll('.dropdown-item').at(1) // Click the second item + await dropdownItem.trigger('click') + + expect(wrapper.emitted().gmsPublishLocation).toBeTruthy() + expect(wrapper.emitted().gmsPublishLocation.length).toBe(1) + expect(wrapper.emitted().gmsPublishLocation[0]).toEqual(['GMS_LOCATION_TYPE_APPROXIMATE']) + }) + + it('does not update when clicking on already selected option', async () => { + const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item (which is already selected) + await dropdownItem.trigger('click') + + expect(wrapper.emitted().gmsPublishLocation).toBeFalsy() + }) + + describe('update with error', () => { + beforeEach(async () => { + mockAPIcall.mockRejectedValue({ + message: 'Ouch', + }) + const dropdownItem = wrapper.findAll('.dropdown-item').at(1) // Click the second item + await dropdownItem.trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Ouch') + }) + }) +}) diff --git a/frontend/src/components/UserSettings/UserGMSLocationFormat.vue b/frontend/src/components/UserSettings/UserGMSLocationFormat.vue new file mode 100644 index 000000000..d20135d5e --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSLocationFormat.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/src/components/UserSettings/UserGMSNamingFormat.spec.js b/frontend/src/components/UserSettings/UserGMSNamingFormat.spec.js new file mode 100644 index 000000000..3dbbfdb2c --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSNamingFormat.spec.js @@ -0,0 +1,81 @@ +import { mount } from '@vue/test-utils' +import UserGMSNamingFormat from './UserGMSNamingFormat.vue' +import { toastErrorSpy } from '@test/testSetup' + +const mockAPIcall = jest.fn() + +const storeCommitMock = jest.fn() + +const localVue = global.localVue + +describe('UserGMSNamingFormat', () => { + let wrapper + beforeEach(() => { + wrapper = mount(UserGMSNamingFormat, { + mocks: { + $t: (key) => key, // Mocking the translation function + $store: { + state: { + gmsPublishName: null, + }, + commit: storeCommitMock, + }, + $apollo: { + mutate: mockAPIcall, + }, + }, + localVue, + propsData: { + selectedOption: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS', + }, + }) + }) + + afterEach(() => { + wrapper.destroy() + }) + + it('renders the correct dropdown options', () => { + const dropdownItems = wrapper.findAll('.dropdown-item') + expect(dropdownItems.length).toBe(5) + + const labels = dropdownItems.wrappers.map((item) => item.text()) + expect(labels).toEqual([ + 'settings.GMS.publish-name.alias-or-initials', + 'settings.GMS.publish-name.initials', + 'settings.GMS.publish-name.first', + 'settings.GMS.publish-name.first-initial', + 'settings.GMS.publish-name.name-full', + ]) + }) + + it('updates selected option on click', async () => { + const dropdownItem = wrapper.findAll('.dropdown-item').at(3) // Click the fourth item + await dropdownItem.trigger('click') + + expect(wrapper.emitted().gmsPublishName).toBeTruthy() + expect(wrapper.emitted().gmsPublishName.length).toBe(1) + expect(wrapper.emitted().gmsPublishName[0]).toEqual(['GMS_PUBLISH_NAME_FIRST_INITIAL']) + }) + + it('does not update when clicking on already selected option', async () => { + const dropdownItem = wrapper.findAll('.dropdown-item').at(0) // Click the first item (which is already selected) + await dropdownItem.trigger('click') + + expect(wrapper.emitted().gmsPublishName).toBeFalsy() + }) + + describe('update with error', () => { + beforeEach(async () => { + mockAPIcall.mockRejectedValue({ + message: 'Ouch', + }) + const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item + await dropdownItem.trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Ouch') + }) + }) +}) diff --git a/frontend/src/components/UserSettings/UserGMSNamingFormat.vue b/frontend/src/components/UserSettings/UserGMSNamingFormat.vue new file mode 100644 index 000000000..29b4cd384 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSNamingFormat.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/UserSettings/UserGMSSwitch.vue b/frontend/src/components/UserSettings/UserGMSSwitch.vue new file mode 100644 index 000000000..355beeff2 --- /dev/null +++ b/frontend/src/components/UserSettings/UserGMSSwitch.vue @@ -0,0 +1,45 @@ + + diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index dd2e85dac..1ead8ecf0 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -8,7 +8,7 @@ const constants = { DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v5.2024-01-08', + EXPECTED: 'v6.2024-02-27', CURRENT: '', }, } @@ -20,6 +20,10 @@ const version = { BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7), } +const features = { + GMS_ACTIVE: process.env.GMS_ACTIVE ?? false, +} + const environment = { NODE_ENV: process.env.NODE_ENV, DEBUG: process.env.NODE_ENV !== 'production' ?? false, @@ -81,6 +85,7 @@ if ( const CONFIG = { ...constants, ...version, + ...features, ...environment, ...endpoints, ...community, diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index cade098da..8d28bbff6 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -35,9 +35,9 @@ export const updateUserInfos = gql` $hideAmountGDD: Boolean $hideAmountGDT: Boolean $gmsAllowed: Boolean - $gmsPublishName: Int + $gmsPublishName: GmsPublishNameType $gmsLocation: Location - $gmsPublishLocation: Int + $gmsPublishLocation: GmsPublishLocationType ) { updateUserInfos( firstName: $firstName @@ -172,6 +172,9 @@ export const login = gql` klickTipp { newsletterState } + gmsAllowed + gmsPublishName + gmsPublishLocation hasElopage publisherId roles diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 337722940..1c37d5e24 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -5,8 +5,10 @@ "1000thanks": "1000 Dank, weil du bei uns bist!", "125": "125%", "85": "85%", + "ExternServices": "Verknüpfte Dienste", "GDD": "GDD", "GDT": "GDT", + "GMS": "Gradido Karte", "PersonalDetails": "Persönliche Angaben", "advanced-calculation": "Vorausberechnung", "asterisks": "****", @@ -290,6 +292,36 @@ }, "settings": { "emailInfo": "Kann aktuell noch nicht geändert werden.", + "GMS": { + "disabled": "Daten werden nicht nach GMS exportiert", + "enabled": "Daten werden nach GMS exportiert", + "location": { + "label": "Positionsbestimmung", + "button": "Klick mich!" + }, + "location-format": "Positionstyp", + "naming-format": "Namensformat im GMS", + "publish-location": { + "exact": "Genaue Position", + "approximate": "Ungefähre Position", + "random": "Zufallsposition", + "updated": "Positionstyp für GMS aktualisiert" + }, + "publish-name": { + "alias-or-initials": "Benutzername oder Initialen", + "alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname", + "first": "Vorname", + "first-tooltip": "Nur der Vornamen", + "first-initial": "Vorname und Initiale", + "first-initial-tooltip": "Vornamen plus Anfangsbuchstabe des Nachnamens", + "initials": "Initialen", + "initials-tooltip": "Initialen von Vor- und Nachname unabhängig von der Existenz des Benutzernamens", + "name-full": "Ganzer Name", + "name-full-tooltip": "Vollständiger Name: Vorname plus Nachname", + "updated": "Namensformat für GMS aktualisiert" + }, + "switch": "Erlaubnis Daten nach GMS zu exportieren." + }, "hideAmountGDD": "Dein GDD Betrag ist versteckt.", "hideAmountGDT": "Dein GDT Betrag ist versteckt.", "info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 982d22159..7c607ecbb 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -5,8 +5,10 @@ "1000thanks": "1000 thanks for being with us!", "125": "125%", "85": "85%", + "ExternServices": "Extern Services", "GDD": "GDD", "GDT": "GDT", + "GMS": "Gradido Map", "PersonalDetails": "Personal details", "advanced-calculation": "Advanced calculation", "asterisks": "****", @@ -290,6 +292,36 @@ }, "settings": { "emailInfo": "Cannot be changed at this time.", + "GMS": { + "disabled": "Data not exported to GMS", + "enabled": "Data exported to GMS", + "location": { + "label": "pinpoint location", + "button": "click me!" + }, + "location-format": "location type", + "naming-format": "Format of name in GMS", + "publish-location": { + "exact": "exact position", + "approximate": "approximate position", + "random": "random position", + "updated": "format of location for GMS updated" + }, + "publish-name": { + "alias-or-initials": "Username or initials", + "alias-or-initials-tooltip": "username if exists or Initials of firstname and lastname", + "first": "firstname", + "first-tooltip": "the firstname only", + "first-initial": "firstname and initial", + "first-initial-tooltip": "firstname plus initial of lastname", + "initials": "Initials of firstname and lastname independent if username exists", + "initials-tooltip": "Initials of firstname and lastname independent if username exists", + "name-full": "fullname", + "name-full-tooltip": "fullname: firstname plus lastname", + "updated": "format of name for GMS updated" + }, + "switch": "Allow data export to GMS" + }, "hideAmountGDD": "Your GDD amount is hidden.", "hideAmountGDT": "Your GDT amount is hidden.", "info": "Transactions can now be made by username or email address.", diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index 1530e5e97..c74af6679 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -1,81 +1,129 @@