Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/AppInfo/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@
namespace OCA\Zulip\AppInfo;

use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;

/** @template-implements IEventListener<BeforeTemplateRenderedEvent|Event> */
class BeforeTemplateRenderedListener implements IEventListener {

public function __construct(
private IConfig $config,
private IInitialState $initialStateService,
) {
}

public function handle(Event $event): void {
if (!($event instanceof BeforeTemplateRenderedEvent)) {
return;
Expand All @@ -18,5 +27,13 @@ public function handle(Event $event): void {
return;
}
\OCP\Util::addStyle(Application::APP_ID, 'zulip-search');

$adminConfig = [
'send_type_enabled_file' => $this->config->getAppValue(Application::APP_ID, 'send_type_enabled_file', '1') === '1',
'send_type_enabled_public_link' => $this->config->getAppValue(Application::APP_ID, 'send_type_enabled_public_link', '1') === '1',
'send_type_enabled_internal_link' => $this->config->getAppValue(Application::APP_ID, 'send_type_enabled_internal_link', '1') === '1',
'send_type_default' => $this->config->getAppValue(Application::APP_ID, 'send_type_default', ''),
];
$this->initialStateService->provideInitialState('admin-config', $adminConfig);
Comment on lines +31 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would read and inject these values in every page of nextcloud so is a bit wasteful
it would be better to add a #[NoAdminRequired] path in ConfigController.php to read them from the filesplugin.js file

}
}
17 changes: 17 additions & 0 deletions lib/Controller/ConfigController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use OCA\Zulip\Service\SecretService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
Expand Down Expand Up @@ -78,6 +79,22 @@ public function setConfig(array $values): DataResponse {
return new DataResponse([]);
}

/**
* @return DataResponse
*/
#[AuthorizedAdminSetting(settings: \OCA\Zulip\Settings\Admin::class)]
#[FrontpageRoute(verb: 'PUT', url: '/admin-config')]
public function setAdminConfig(array $values): DataResponse {
$allowedKeys = ['send_type_enabled_file', 'send_type_enabled_public_link', 'send_type_enabled_internal_link', 'send_type_default'];
foreach ($values as $key => $value) {
if (!in_array($key, $allowedKeys, true)) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$this->config->setAppValue(Application::APP_ID, $key, $value);
}
return new DataResponse([]);
}

/**
* set sensitive config values
*
Expand Down
39 changes: 39 additions & 0 deletions lib/Settings/Admin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace OCA\Zulip\Settings;

use OCA\Zulip\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\Settings\ISettings;

class Admin implements ISettings {

public function __construct(
private IConfig $config,
private IInitialState $initialStateService,
) {
}

public function getForm(): TemplateResponse {
$adminConfig = [
'send_type_enabled_file' => $this->config->getAppValue(Application::APP_ID, 'send_type_enabled_file', '1') === '1',
'send_type_enabled_public_link' => $this->config->getAppValue(Application::APP_ID, 'send_type_enabled_public_link', '1') === '1',
'send_type_enabled_internal_link' => $this->config->getAppValue(Application::APP_ID, 'send_type_enabled_internal_link', '1') === '1',
'send_type_default' => $this->config->getAppValue(Application::APP_ID, 'send_type_default', ''),
];
$this->initialStateService->provideInitialState('admin-config', $adminConfig);
return new TemplateResponse(Application::APP_ID, 'adminSettings');
}

public function getSection(): string {
return 'connected-accounts';
}

public function getPriority(): int {
return 10;
}
}
34 changes: 34 additions & 0 deletions lib/Settings/AdminSection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace OCA\Zulip\Settings;

use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;

class AdminSection implements IIconSection {

public function __construct(
private IURLGenerator $urlGenerator,
private IL10N $l,
) {
}

public function getID(): string {
return 'connected-accounts';
}

public function getName(): string {
return $this->l->t('Connected accounts');
}

public function getPriority(): int {
return 80;
}

public function getIcon(): string {
return $this->urlGenerator->imagePath('core', 'categories/integration.svg');
}
}
6 changes: 6 additions & 0 deletions src/adminSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import AdminSettings from './components/AdminSettings.vue'

const app = createApp(AdminSettings)
app.mixin({ methods: { t, n } })
app.mount('#zulip_prefs')
197 changes: 197 additions & 0 deletions src/components/AdminSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<template>
<div id="zulip_prefs" class="section">
<h2>
<ZulipIcon class="icon" />
{{ t('integration_zulip', 'Zulip integration') }}
</h2>
<div id="zulip-content">
<h3>{{ t('integration_zulip', 'File sharing options') }}</h3>
<p class="settings-hint">
{{ t('integration_zulip', 'Configure which sharing options are available to users when sending files to Zulip.') }}
</p>
<NcFormBox>
<NcFormBoxSwitch
v-model="state.send_type_enabled_file"
:disabled="isOnlyEnabled('file')"
@update:model-value="onCheckboxChanged($event, 'send_type_enabled_file')">
{{ t('integration_zulip', 'Allow uploading files') }}
</NcFormBoxSwitch>
<NcFormBoxSwitch
v-model="state.send_type_enabled_public_link"
:disabled="isOnlyEnabled('public_link')"
@update:model-value="onCheckboxChanged($event, 'send_type_enabled_public_link')">
{{ t('integration_zulip', 'Allow sending public links') }}
</NcFormBoxSwitch>
<NcFormBoxSwitch
v-model="state.send_type_enabled_internal_link"
:disabled="isOnlyEnabled('internal_link')"
@update:model-value="onCheckboxChanged($event, 'send_type_enabled_internal_link')">
{{ t('integration_zulip', 'Allow sending internal links') }}
</NcFormBoxSwitch>
</NcFormBox>
<div v-if="enabledSendTypes.length > 1" class="default-type-section">
<h3>{{ t('integration_zulip', 'Default sharing option') }}</h3>
<p class="settings-hint">
{{ t('integration_zulip', 'Select which option is pre-selected when users open the sharing dialog.') }}
Comment on lines +32 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about having this section still visible when only one send option is available?
it could just show that one option selected to be more explicit but maybe that is already quite evident from the list above?

@Thomas-git Thomas-git Jul 7, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's pretty obvious, that's why it's hidden. But maybe a better UI design would be to have this part always visible and to disable unavailable choices?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that could work too.
but it's just a nitpick, feel free to ignore if the current design feels good to you.

</p>
<div class="default-type-radios">
<NcCheckboxRadioSwitch
v-model="defaultSendType"
value="first_available"
name="default_send_type_radio"
type="radio">
{{ t('integration_zulip', 'First available option') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
v-if="state.send_type_enabled_file"
v-model="defaultSendType"
value="file"
name="default_send_type_radio"
type="radio">
{{ t('integration_zulip', 'Upload files') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
v-if="state.send_type_enabled_public_link"
v-model="defaultSendType"
value="public_link"
name="default_send_type_radio"
type="radio">
{{ t('integration_zulip', 'Public links') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
v-if="state.send_type_enabled_internal_link"
v-model="defaultSendType"
value="internal_link"
name="default_send_type_radio"
type="radio">
{{ t('integration_zulip', 'Internal links') }}
</NcCheckboxRadioSwitch>
</div>
</div>
</div>
</div>
</template>

<script>
import ZulipIcon from './icons/ZulipIcon.vue'

import NcFormBox from '@nextcloud/vue/components/NcFormBox'
import NcFormBoxSwitch from '@nextcloud/vue/components/NcFormBoxSwitch'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'

import axios from '@nextcloud/axios'
import { showSuccess, showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { generateUrl } from '@nextcloud/router'

export default {
name: 'AdminSettings',

components: {
ZulipIcon,
NcFormBox,
NcFormBoxSwitch,
NcCheckboxRadioSwitch,
},

data() {
return {
state: loadState('integration_zulip', 'admin-config'),
}
},

computed: {
enabledSendTypes() {
return [
this.state.send_type_enabled_file && 'file',
this.state.send_type_enabled_public_link && 'public_link',
this.state.send_type_enabled_internal_link && 'internal_link',
].filter(Boolean)
},
defaultSendType: {
get() {
return this.state.send_type_default === '' ? 'first_available' : this.state.send_type_default
},
set(value) {
this.onDefaultTypeChanged(value === 'first_available' ? '' : value)
},
},
},

watch: {
enabledSendTypes(newTypes) {
if (this.state.send_type_default !== '' && !newTypes.includes(this.state.send_type_default)) {
this.onDefaultTypeChanged('')
}
},
},

methods: {
isOnlyEnabled(key) {
return this.enabledSendTypes.length === 1 && this.enabledSendTypes[0] === key
},
onCheckboxChanged(newValue, key) {
this.saveAdminConfig({ [key]: newValue ? '1' : '0' })
},
onDefaultTypeChanged(value) {
this.state.send_type_default = value
this.saveAdminConfig({ send_type_default: value })
},
saveAdminConfig(values) {
const req = { values }
const url = generateUrl('/apps/integration_zulip/admin-config')
axios.put(url, req)
.then(() => {
showSuccess(t('integration_zulip', 'Zulip admin options saved'))
})
.catch((error) => {
showError(
t('integration_zulip', 'Failed to save Zulip admin options')
+ ': ' + (error.response?.request?.responseText ?? ''),
)
console.error(error)
})
},
},
}
</script>

<style scoped lang="scss">
#zulip_prefs {
#zulip-content {
margin-left: 40px;
display: flex;
flex-direction: column;
gap: 4px;
max-width: 800px;
}

h2 {
display: flex;
align-items: center;
gap: 8px;
justify-content: start;
}

h3 {
margin-top: 16px;
margin-bottom: 4px;
}

.settings-hint {
color: var(--color-text-maxcontrast);
margin-bottom: 8px;
}

.default-type-section {
margin-top: 16px;
}

.default-type-radios {
margin-left: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
}
</style>
Loading