mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 01:46:07 +00:00
choose space in modal
This commit is contained in:
parent
ce08b2d843
commit
a3147661c3
117
admin/src/components/ProjectBranding/ListHumhubSpaces.vue
Normal file
117
admin/src/components/ProjectBranding/ListHumhubSpaces.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="list-unstyled list-group mb-3">
|
||||
<li
|
||||
v-for="space in spaces"
|
||||
:key="space.id"
|
||||
:title="space.description"
|
||||
:class="[
|
||||
'list-group-item',
|
||||
'list-group-item-action',
|
||||
'd-flex',
|
||||
'justify-content-between',
|
||||
'align-items-center',
|
||||
'cursor-pointer',
|
||||
{ active: space.id === selectedSpaceId },
|
||||
]"
|
||||
@click="selectedSpaceId = space.id"
|
||||
>
|
||||
<div>
|
||||
<input v-model="selectedSpaceId" type="radio" :value="space.id" class="me-2" />
|
||||
{{ space.name }}
|
||||
</div>
|
||||
<a :href="space.url" target="_blank" @click.stop>
|
||||
{{ $t('projectBranding.openSpaceInHumhub') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<b-pagination
|
||||
v-if="result && result.spaces.total > ITEMS_PER_PAGE"
|
||||
v-model="result.spaces.page"
|
||||
:total-rows="result.spaces.total"
|
||||
:per-page="ITEMS_PER_PAGE"
|
||||
aria-controls="list-humhub-spaces"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const ITEMS_PER_PAGE = 20
|
||||
const page = ref(1)
|
||||
const selectedSpaceId = ref(props.modelValue)
|
||||
const { result, refetch } = useQuery(
|
||||
gql`
|
||||
query spaces($page: Int!) {
|
||||
spaces(page: $page) {
|
||||
total
|
||||
page
|
||||
pages
|
||||
results {
|
||||
id
|
||||
name
|
||||
description
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ page: page.value, limit: ITEMS_PER_PAGE },
|
||||
)
|
||||
|
||||
const spaces = computed(() => result.value?.spaces?.results || [])
|
||||
|
||||
watch(
|
||||
() => selectedSpaceId.value,
|
||||
(newValue) => emit('update:modelValue', newValue),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
console.log('space id updated, new value:', newValue)
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
console.log('on mounted', props.modelValue)
|
||||
if (props.spaceId) {
|
||||
const targetPage = Math.ceil(props.spaceId / ITEMS_PER_PAGE)
|
||||
page.value = targetPage
|
||||
refetch({ page: targetPage })
|
||||
}
|
||||
})
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value -= 1
|
||||
refetch({ page: page.value })
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (hasMore.value) {
|
||||
page.value += 1
|
||||
refetch({ page: page.value })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.list-group-item-action:hover:not(.active) {
|
||||
background-color: #ececec;
|
||||
color: #0056b3;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@ -26,6 +26,14 @@
|
||||
class="mb-3"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<BButton
|
||||
variant="outline-secondary"
|
||||
class="mb-3"
|
||||
:title="result?.space.description"
|
||||
@click="isModalVisible = true"
|
||||
>
|
||||
{{ selectedSpaceText }}
|
||||
</BButton>
|
||||
<BFormGroup
|
||||
:label="$t('projectBranding.newUserToSpace')"
|
||||
label-for="newUserToSpace-input-field"
|
||||
@ -58,20 +66,32 @@
|
||||
<BButton type="reset" variant="secondary" @click="resetForm">{{ $t('reset') }}</BButton>
|
||||
</div>
|
||||
</BForm>
|
||||
<BModal v-model="isModalVisible" title="Select Space" hide-footer>
|
||||
<ListHumhubSpaces
|
||||
:model-value="spaceId"
|
||||
@update:model-value="(value) => updateField(value, 'spaceId')"
|
||||
/>
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ValidatedInput from '@/components/input/ValidatedInput'
|
||||
import ListHumhubSpaces from '@/components/ProjectBranding/ListHumhubSpaces.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { reactive, computed, watch, ref } from 'vue'
|
||||
import { object, string, boolean } from 'yup'
|
||||
import { object, string, boolean, number } from 'yup'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const form = reactive({ ...props.modelValue })
|
||||
const isModalVisible = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const { t } = useI18n()
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => Object.assign(form, newValue),
|
||||
@ -79,8 +99,32 @@ watch(
|
||||
const name = computed(() => form.name)
|
||||
const alias = computed(() => form.alias)
|
||||
const description = computed(() => form.description)
|
||||
const spaceId = computed(() => form.spaceId)
|
||||
const newUserToSpace = computed(() => form.newUserToSpace)
|
||||
const logoUrl = computed(() => form.logoUrl)
|
||||
// show space
|
||||
const GET_SPACE = gql`
|
||||
query space($id: ID!) {
|
||||
space(id: $id) {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
const { result, loading } = useQuery(GET_SPACE, () => ({ id: spaceId.value }), {
|
||||
enabled: computed(() => !!spaceId.value),
|
||||
})
|
||||
|
||||
const selectedSpaceText = computed(() => {
|
||||
console.log('selectedSpaceText: ', result.value)
|
||||
if (!spaceId.value) {
|
||||
return t('projectBranding.selectSpace')
|
||||
}
|
||||
if (!result.value?.space) {
|
||||
return t('projectBranding.noAccessRightSpace', { spaceId: spaceId.value })
|
||||
}
|
||||
return t('projectBranding.chosenSpace', { space: result.value.space.name })
|
||||
})
|
||||
|
||||
const validationSchema = object({
|
||||
name: string().min(3).max(255).required(),
|
||||
@ -92,12 +136,14 @@ const validationSchema = object({
|
||||
.max(32)
|
||||
.required(),
|
||||
description: string().nullable().optional(),
|
||||
spaceId: number().nullable().optional(),
|
||||
newUserToSpace: boolean().optional(),
|
||||
logoUrl: string().url('Logo URL must be a valid URL.').max(255).nullable().optional(),
|
||||
})
|
||||
|
||||
function updateField(value, name) {
|
||||
form[name] = value
|
||||
console.log('updateField called with', { value, name })
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
function submit() {
|
||||
@ -117,6 +163,7 @@ function resetForm() {
|
||||
name: '',
|
||||
alias: '',
|
||||
description: undefined,
|
||||
spaceId: undefined,
|
||||
newUserToSpace: false,
|
||||
logoUrl: undefined,
|
||||
})
|
||||
|
||||
@ -1,9 +1,28 @@
|
||||
<template>
|
||||
<div class="project-branding-item">
|
||||
<BRow :title="item.description" @click="details = !details">
|
||||
<BCol cols="3">{{ item.name }}</BCol>
|
||||
<BCol cols="3">
|
||||
{{ item.name }}
|
||||
<br />
|
||||
{{ frontendLoginUrl }}
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
variant="secondary"
|
||||
:title="$t('copy-to-clipboard')"
|
||||
@click.stop="copyToClipboard(frontendLoginUrl)"
|
||||
>
|
||||
<i class="fas fa-copy"></i>
|
||||
</BButton>
|
||||
</BCol>
|
||||
<BCol cols="2">{{ item.alias }}</BCol>
|
||||
<BCol cols="2">{{ item.newUserToSpace }}</BCol>
|
||||
<BCol cols="2">
|
||||
<span v-if="item.newUserToSpace" class="text-success">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span v-else class="text-danger">
|
||||
<i class="fas fa-times"></i>
|
||||
</span>
|
||||
</BCol>
|
||||
<BCol cols="3" class="me-2">
|
||||
<img class="img-fluid" :src="item.logoUrl" :alt="item.logoUrl" />
|
||||
</BCol>
|
||||
@ -24,25 +43,47 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, toRefs } from 'vue'
|
||||
import { computed, ref, toRefs } from 'vue'
|
||||
import ProjectBrandingForm from './ProjectBrandingForm.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import CONFIG from '@/config'
|
||||
import gql from 'graphql-tag'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { toastSuccess, toastError } = useAppToast()
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
})
|
||||
const { item } = toRefs(props)
|
||||
const details = ref(false)
|
||||
|
||||
const emit = defineEmits(['update:item', 'deleted:item'])
|
||||
const frontendLoginUrl = computed(() => {
|
||||
return `${CONFIG.WALLET_LOGIN_URL}?project=${item.value.alias}`
|
||||
})
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
toastSuccess(t('copied-to-clipboard'))
|
||||
} catch (err) {
|
||||
toastError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function update(form) {
|
||||
details.value = false
|
||||
const { mutate } = useMutation(gql`
|
||||
mutation upsertProjectBranding($input: ProjectBrandingInput!) {
|
||||
upsertProjectBranding(input: $input) {
|
||||
id
|
||||
name
|
||||
alias
|
||||
description
|
||||
spaceId
|
||||
newUserToSpace
|
||||
logoUrl
|
||||
}
|
||||
|
||||
@ -44,6 +44,8 @@
|
||||
"denied": "Abgelehnt",
|
||||
"open": "Offen"
|
||||
},
|
||||
"copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"copied-to-clipboard": "In die Zwischenablage kopiert",
|
||||
"created": "Geschöpft",
|
||||
"createdAt": "Angelegt",
|
||||
"creation": "Schöpfung",
|
||||
@ -129,6 +131,7 @@
|
||||
"lastname": "Nachname",
|
||||
"latitude": "Breitengrad:",
|
||||
"latitude-longitude-smart": "Breitengrad, Längengrad",
|
||||
"link": "Link",
|
||||
"logo": "Logo",
|
||||
"longitude": "Längengrad:",
|
||||
"math": {
|
||||
@ -206,6 +209,11 @@
|
||||
},
|
||||
"projectBranding": {
|
||||
"addTooltip": "Neuen Projekt Branding Eintrag hinzufügen",
|
||||
"chosenSpace": "Gewählter Space: {space}",
|
||||
"noAccessRightSpace": "Gewählter Space: {spaceId} (Keine Zugriffsrechte)",
|
||||
"openSpaceInHumhub": "In Humhub öffnen",
|
||||
"spaceId": "Humhub Space ID",
|
||||
"selectSpace": "Humhub Space auswählen",
|
||||
"title": "Projekt Brandings",
|
||||
"newUserToSpace": "Benutzer hinzufügen?",
|
||||
"newUserToSpaceTooltip": "Neue Benutzer automatisch zum Space hinzufügen, falls Space vorhanden"
|
||||
|
||||
@ -44,6 +44,8 @@
|
||||
"denied": "Rejected",
|
||||
"open": "Open"
|
||||
},
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"created": "Created for",
|
||||
"createdAt": "Created at",
|
||||
"creation": "Creation",
|
||||
@ -130,6 +132,7 @@
|
||||
"lastname": "Lastname",
|
||||
"latitude": "Latitude:",
|
||||
"latitude-longitude-smart": "Latitude, Longitude",
|
||||
"link": "Link",
|
||||
"logo": "Logo",
|
||||
"longitude": "Longitude:",
|
||||
"math": {
|
||||
@ -207,6 +210,11 @@
|
||||
},
|
||||
"projectBranding": {
|
||||
"addTooltip": "Add new project branding entry",
|
||||
"chosenSpace": "Choosen Humhub Space: {space}",
|
||||
"noAccessRightSpace": "Selected space: {spaceId} (No access rights)",
|
||||
"openSpaceInHumhub": "Open in Humhub",
|
||||
"spaceId": "Humhub Space ID",
|
||||
"selectSpace": "Select Humhub Space",
|
||||
"title": "Project Branding",
|
||||
"newUserToSpace": "Add user?",
|
||||
"newUserToSpaceTooltip": "The hours should contain a maximum of two decimal places"
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<BListGroup>
|
||||
<BRow>
|
||||
<BCol cols="3" class="ms-1">{{ $t('name') }}</BCol>
|
||||
<BCol cols="3" class="ms-1">{{ $t('name') }} + {{ $t('link') }}</BCol>
|
||||
<BCol cols="2">{{ $t('alias') }}</BCol>
|
||||
<BCol cols="2" :title="$t('projectBranding.newUserToSpaceTooltip')">
|
||||
{{ $t('projectBranding.newUserToSpace') }}
|
||||
|
||||
@ -8,7 +8,9 @@ import { backendLogger as logger } from '@/server/logger'
|
||||
import { PostUserLoggingView } from './logging/PostUserLogging.view'
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { PostUser } from './model/PostUser'
|
||||
import { SpacesResponse } from './model/SpacesResponse'
|
||||
import { UsersResponse } from './model/UsersResponse'
|
||||
import { Space } from './model/Space'
|
||||
|
||||
/**
|
||||
* HumHubClient as singleton class
|
||||
@ -186,6 +188,29 @@ export class HumHubClient {
|
||||
throw new LogError('error deleting user', { userId: humhubUserId, response })
|
||||
}
|
||||
}
|
||||
|
||||
// get spaces from humhub
|
||||
// https://marketplace.humhub.com/module/rest/docs/html/space.html#tag/Space/paths/~1space/get
|
||||
public async spaces(page = 0, limit = 20): Promise<SpacesResponse | null> {
|
||||
const options = await this.createRequestOptions({ page, limit })
|
||||
const response = await this.restClient.get<SpacesResponse>('/api/v1/space', options)
|
||||
if (response.statusCode !== 200) {
|
||||
throw new LogError('error requesting spaces from humhub', response)
|
||||
}
|
||||
return response.result
|
||||
}
|
||||
|
||||
// get space by id from humhub instance
|
||||
// https://marketplace.humhub.com/module/rest/docs/html/space.html#tag/Space/paths/~1space~1{id}/get
|
||||
public async space(spaceId: number): Promise<Space | null> {
|
||||
const options = await this.createRequestOptions()
|
||||
const response = await this.restClient.get<Space>(`/api/v1/space/${spaceId}`, options)
|
||||
console.log(response)
|
||||
if (response.statusCode !== 200) {
|
||||
throw new LogError('error requesting space from humhub', response)
|
||||
}
|
||||
return response.result
|
||||
}
|
||||
}
|
||||
|
||||
// new RestClient('gradido', 'api/v1/')
|
||||
|
||||
7
backend/src/apis/humhub/model/Space.ts
Normal file
7
backend/src/apis/humhub/model/Space.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class Space {
|
||||
id: number
|
||||
guid: string
|
||||
name: string
|
||||
description: string
|
||||
url: string
|
||||
}
|
||||
8
backend/src/apis/humhub/model/SpacesResponse.ts
Normal file
8
backend/src/apis/humhub/model/SpacesResponse.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Space } from './Space'
|
||||
|
||||
export interface SpacesResponse {
|
||||
total: number
|
||||
page: number
|
||||
pages: number
|
||||
results: Space[]
|
||||
}
|
||||
@ -41,6 +41,7 @@ export enum RIGHTS {
|
||||
GMS_USER_PLAYGROUND = 'GMS_USER_PLAYGROUND',
|
||||
HUMHUB_AUTO_LOGIN = 'HUMHUB_AUTO_LOGIN',
|
||||
PROJECT_BRANDING_VIEW = 'PROJECT_BRANDING_VIEW',
|
||||
LIST_HUMHUB_SPACES = 'LIST_HUMHUB_SPACES',
|
||||
// Moderator
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
|
||||
|
||||
@ -32,4 +32,5 @@ export const USER_RIGHTS = [
|
||||
RIGHTS.GMS_USER_PLAYGROUND,
|
||||
RIGHTS.HUMHUB_AUTO_LOGIN,
|
||||
RIGHTS.PROJECT_BRANDING_VIEW,
|
||||
RIGHTS.LIST_HUMHUB_SPACES,
|
||||
]
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { ProjectBranding as dbProjectBranding } from '@entity/ProjectBranding'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { Space } from './Space'
|
||||
|
||||
@ObjectType()
|
||||
export class ProjectBranding {
|
||||
constructor(projectBranding: dbProjectBranding) {
|
||||
@ -22,6 +24,9 @@ export class ProjectBranding {
|
||||
@Field(() => Int, { nullable: true })
|
||||
spaceId: number | null
|
||||
|
||||
@Field(() => Space, { nullable: true })
|
||||
space: Space | null
|
||||
|
||||
@Field(() => Boolean)
|
||||
newUserToSpace: boolean
|
||||
|
||||
|
||||
25
backend/src/graphql/model/Space.ts
Normal file
25
backend/src/graphql/model/Space.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { Space as HumhubSpace } from '@/apis/humhub/model/Space'
|
||||
|
||||
@ObjectType()
|
||||
export class Space {
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
guid: string
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
|
||||
@Field(() => String)
|
||||
description: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
|
||||
constructor(data: HumhubSpace) {
|
||||
Object.assign(this, data)
|
||||
}
|
||||
}
|
||||
24
backend/src/graphql/model/SpaceList.ts
Normal file
24
backend/src/graphql/model/SpaceList.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { SpacesResponse } from '@/apis/humhub/model/SpacesResponse'
|
||||
|
||||
import { Space } from './Space'
|
||||
|
||||
@ObjectType()
|
||||
export class SpaceList {
|
||||
@Field(() => Int)
|
||||
total: number
|
||||
|
||||
@Field(() => Int)
|
||||
page: number
|
||||
|
||||
@Field(() => Int)
|
||||
pages: number
|
||||
|
||||
@Field(() => [Space])
|
||||
results: Space[]
|
||||
|
||||
constructor(data: SpacesResponse) {
|
||||
Object.assign(this, data)
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,17 @@
|
||||
import { ProjectBranding as DbProjectBranding } from '@entity/ProjectBranding'
|
||||
import { Resolver, Query, Mutation, Arg, Int, Authorized, ID } from 'type-graphql'
|
||||
import { Resolver, Query, Mutation, Arg, Int, Authorized, ID, FieldResolver, Root } from 'type-graphql'
|
||||
|
||||
import { ProjectBrandingInput } from '@input/ProjectBrandingInput'
|
||||
import { ProjectBranding } from '@model/ProjectBranding'
|
||||
import { Space } from '@model/Space'
|
||||
import { SpaceList } from '@model/SpaceList'
|
||||
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
@Resolver()
|
||||
@Resolver(() => ProjectBranding)
|
||||
export class ProjectBrandingResolver {
|
||||
@Query(() => [ProjectBranding])
|
||||
@Authorized([RIGHTS.PROJECT_BRANDING_VIEW])
|
||||
@ -67,4 +70,37 @@ export class ProjectBrandingResolver {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => Space)
|
||||
@Authorized([RIGHTS.LIST_HUMHUB_SPACES])
|
||||
async space(@Arg('id', () => ID) id: number): Promise<Space> {
|
||||
const humhub = HumHubClient.getInstance()
|
||||
if (!humhub) {
|
||||
throw new LogError('HumHub client not initialized')
|
||||
}
|
||||
|
||||
const space = await humhub.space(id)
|
||||
if (!space) {
|
||||
throw new LogError(`Error requesting space with id: ${id} from HumHub`)
|
||||
}
|
||||
return new Space(space)
|
||||
}
|
||||
|
||||
@Query(() => SpaceList)
|
||||
@Authorized([RIGHTS.LIST_HUMHUB_SPACES])
|
||||
async spaces(
|
||||
@Arg('page', () => Int, { defaultValue: 1 }) page: number,
|
||||
@Arg('limit', () => Int, { defaultValue: 20 }) limit: number,
|
||||
): Promise<SpaceList> {
|
||||
const humhub = HumHubClient.getInstance()
|
||||
if (!humhub) {
|
||||
throw new LogError('HumHub client not initialized')
|
||||
}
|
||||
const offset = (page - 1) * limit
|
||||
const spaces = await humhub.spaces(offset, limit)
|
||||
if (!spaces) {
|
||||
throw new LogError('Error requesting spaces from HumHub')
|
||||
}
|
||||
return new SpaceList(spaces)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user