choose space in modal

This commit is contained in:
einhornimmond 2025-02-17 17:43:41 +01:00
parent ce08b2d843
commit a3147661c3
15 changed files with 360 additions and 7 deletions

View 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>

View File

@ -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,
})

View File

@ -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
}

View File

@ -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"

View File

@ -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"

View File

@ -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') }}

View File

@ -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/')

View File

@ -0,0 +1,7 @@
export class Space {
id: number
guid: string
name: string
description: string
url: string
}

View File

@ -0,0 +1,8 @@
import { Space } from './Space'
export interface SpacesResponse {
total: number
page: number
pages: number
results: Space[]
}

View File

@ -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',

View File

@ -32,4 +32,5 @@ export const USER_RIGHTS = [
RIGHTS.GMS_USER_PLAYGROUND,
RIGHTS.HUMHUB_AUTO_LOGIN,
RIGHTS.PROJECT_BRANDING_VIEW,
RIGHTS.LIST_HUMHUB_SPACES,
]

View File

@ -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

View 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)
}
}

View 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)
}
}

View File

@ -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)
}
}