refactor: use vuetify form controls
This commit is contained in:
26
docs/TASKS/frontend/001-vuetify-native-form-controls.md
Normal file
26
docs/TASKS/frontend/001-vuetify-native-form-controls.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Task: Replace native form controls with Vuetify controls
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move interactive form fields from native `input`, `select`, and `textarea` elements to Vuetify form components so form theming flows through `createVuetify`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Replace native text, email, URL, search, date, and number inputs with `v-text-field`.
|
||||||
|
- Replace native selects with `v-select`.
|
||||||
|
- Replace native textareas with `v-textarea`.
|
||||||
|
- Replace native checkboxes/radios with Vuetify selection controls where practical.
|
||||||
|
- Preserve file inputs where Vuetify would reduce custom upload behavior.
|
||||||
|
- Keep custom navigation and row action buttons out of this pass unless they are part of a form.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Done
|
||||||
|
|
||||||
|
- [x] Native form controls under `frontend/src/**/*.vue` were replaced with Vuetify form components.
|
||||||
|
- [x] Frontend build passes.
|
||||||
@@ -44,7 +44,6 @@
|
|||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'save']);
|
const emit = defineEmits(['update:modelValue', 'save']);
|
||||||
|
|
||||||
const fileInput = ref(null);
|
|
||||||
const cropper = ref(null);
|
const cropper = ref(null);
|
||||||
const imageUrl = ref(null);
|
const imageUrl = ref(null);
|
||||||
const remoteUrl = ref('');
|
const remoteUrl = ref('');
|
||||||
@@ -67,17 +66,11 @@
|
|||||||
imageUrl.value = props.initialUrl || null;
|
imageUrl.value = props.initialUrl || null;
|
||||||
remoteUrl.value = props.initialUrl || '';
|
remoteUrl.value = props.initialUrl || '';
|
||||||
error.value = null;
|
error.value = null;
|
||||||
if (fileInput.value) {
|
|
||||||
fileInput.value.value = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseImage() {
|
function onFileSelected(value) {
|
||||||
fileInput.value?.click();
|
const file = Array.isArray(value) ? value[0] : value;
|
||||||
}
|
|
||||||
|
|
||||||
function onFileSelected(event) {
|
|
||||||
const [file] = event.target.files ?? [];
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -165,28 +158,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cropper-actions">
|
<div class="cropper-actions">
|
||||||
<input
|
<v-file-input
|
||||||
ref="fileInput"
|
:label="uploadLabel"
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="hidden-input"
|
|
||||||
@change="onFileSelected"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="action-button"
|
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
@click="chooseImage"
|
density="compact"
|
||||||
>
|
variant="outlined"
|
||||||
{{ uploadLabel }}
|
hide-details
|
||||||
</button>
|
@update:model-value="onFileSelected"
|
||||||
|
/>
|
||||||
<div class="url-controls">
|
<div class="url-controls">
|
||||||
<input
|
<v-text-field
|
||||||
v-model="remoteUrl"
|
v-model="remoteUrl"
|
||||||
type="url"
|
type="url"
|
||||||
class="url-input"
|
class="url-input"
|
||||||
:placeholder="sourceLabel"
|
:placeholder="sourceLabel"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="action-button secondary"
|
class="action-button secondary"
|
||||||
|
|||||||
@@ -10,36 +10,25 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<form @submit.prevent="handleForgotPassword">
|
<v-form @submit.prevent="handleForgotPassword">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="form-field">
|
<v-text-field
|
||||||
<label
|
v-model="email"
|
||||||
class="form-label"
|
:label="t('email')"
|
||||||
for="email"
|
required
|
||||||
>
|
type="email"
|
||||||
{{ t('email') }}
|
variant="outlined"
|
||||||
</label>
|
/>
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
v-model="email"
|
|
||||||
class="form-input"
|
|
||||||
required
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<v-btn
|
||||||
:disabled="isLoading"
|
:loading="isLoading"
|
||||||
class="primary w-full"
|
block
|
||||||
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
v-if="isLoading"
|
|
||||||
class="loading-spinner mr-2"
|
|
||||||
></span>
|
|
||||||
{{ t('resetPassword') }}
|
{{ t('resetPassword') }}
|
||||||
</button>
|
</v-btn>
|
||||||
|
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -51,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</v-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success message -->
|
<!-- Success message -->
|
||||||
|
|||||||
@@ -5,68 +5,34 @@
|
|||||||
{{ t('title') }}
|
{{ t('title') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
class="card"
|
class="card"
|
||||||
@submit.prevent="handleResetPassword"
|
@submit.prevent="handleResetPassword"
|
||||||
>
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="form-field">
|
<div>
|
||||||
<label
|
<v-text-field
|
||||||
class="form-label"
|
v-model="password"
|
||||||
for="password"
|
:append-inner-icon="showPassword ? mdiEyeOff : mdiEye"
|
||||||
>
|
:label="t('newPassword')"
|
||||||
{{ t('newPassword') }}
|
:type="showPassword ? 'text' : 'password'"
|
||||||
</label>
|
required
|
||||||
<div class="relative">
|
variant="outlined"
|
||||||
<input
|
@click:append-inner="showPassword = !showPassword"
|
||||||
id="password"
|
/>
|
||||||
v-model="password"
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
class="form-input"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="password-toggle"
|
|
||||||
type="button"
|
|
||||||
@click="showPassword = !showPassword"
|
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
:icon="showPassword ? mdiEyeOff : mdiEye"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">{{ t('passwordRequirements') }}</p>
|
<p class="mt-1 text-sm text-gray-500">{{ t('passwordRequirements') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<v-text-field
|
||||||
<label
|
v-model="confirmPassword"
|
||||||
class="form-label"
|
:append-inner-icon="showConfirmPassword ? mdiEyeOff : mdiEye"
|
||||||
for="confirmPassword"
|
:label="t('confirmPassword')"
|
||||||
>
|
:type="showConfirmPassword ? 'text' : 'password'"
|
||||||
{{ t('confirmPassword') }}
|
required
|
||||||
</label>
|
variant="outlined"
|
||||||
<div class="relative">
|
@click:append-inner="showConfirmPassword = !showConfirmPassword"
|
||||||
<input
|
/>
|
||||||
id="confirmPassword"
|
|
||||||
v-model="confirmPassword"
|
|
||||||
:type="showConfirmPassword ? 'text' : 'password'"
|
|
||||||
class="form-input"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="password-toggle"
|
|
||||||
type="button"
|
|
||||||
@click="showConfirmPassword = !showConfirmPassword"
|
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
:icon="showConfirmPassword ? mdiEyeOff : mdiEye"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
@@ -75,20 +41,17 @@
|
|||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<v-btn
|
||||||
:disabled="isLoading"
|
:loading="isLoading"
|
||||||
class="primary w-full"
|
block
|
||||||
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
v-if="isLoading"
|
|
||||||
class="loading-spinner mr-2"
|
|
||||||
></span>
|
|
||||||
{{ t('resetPassword') }}
|
{{ t('resetPassword') }}
|
||||||
</button>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</v-form>
|
||||||
|
|
||||||
<!-- Success message -->
|
<!-- Success message -->
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -129,48 +129,52 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('campaigns.fields.startDate') }}</span>
|
v-model="form.startDate"
|
||||||
<input
|
:label="t('campaigns.fields.startDate')"
|
||||||
v-model="form.startDate"
|
:disabled="campaignsStore.isCreating"
|
||||||
type="date"
|
type="date"
|
||||||
:disabled="campaignsStore.isCreating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('campaigns.fields.endDate') }}</span>
|
v-model="form.endDate"
|
||||||
<input
|
:label="t('campaigns.fields.endDate')"
|
||||||
v-model="form.endDate"
|
:disabled="campaignsStore.isCreating"
|
||||||
type="date"
|
type="date"
|
||||||
:disabled="campaignsStore.isCreating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>{{ t('campaigns.fields.name') }}</span>
|
v-model="form.name"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.name"
|
:label="t('campaigns.fields.name')"
|
||||||
type="text"
|
:disabled="campaignsStore.isCreating"
|
||||||
:disabled="campaignsStore.isCreating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-textarea
|
||||||
<span>{{ t('campaigns.fields.description') }}</span>
|
v-model="form.description"
|
||||||
<textarea
|
class="field-wide"
|
||||||
v-model="form.description"
|
:label="t('campaigns.fields.description')"
|
||||||
:disabled="campaignsStore.isCreating"
|
:disabled="campaignsStore.isCreating"
|
||||||
></textarea>
|
rows="3"
|
||||||
</label>
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-textarea
|
||||||
<span>{{ t('campaigns.fields.notes') }}</span>
|
v-model="form.notes"
|
||||||
<textarea
|
class="field-wide"
|
||||||
v-model="form.notes"
|
:label="t('campaigns.fields.notes')"
|
||||||
:disabled="campaignsStore.isCreating"
|
:disabled="campaignsStore.isCreating"
|
||||||
></textarea>
|
rows="3"
|
||||||
</label>
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
|
|||||||
@@ -164,13 +164,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('channels.fields.name') }}</span>
|
v-model="form.name"
|
||||||
<input
|
:label="t('channels.fields.name')"
|
||||||
v-model="form.name"
|
variant="outlined"
|
||||||
type="text"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
|
|||||||
@@ -280,26 +280,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>Client name</span>
|
v-model="form.name"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.name"
|
label="Client name"
|
||||||
type="text"
|
:disabled="clientsStore.isUpdating"
|
||||||
:disabled="clientsStore.isUpdating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>Status</span>
|
v-model="form.status"
|
||||||
<select
|
:items="['Active', 'Paused', 'Archived']"
|
||||||
v-model="form.status"
|
label="Status"
|
||||||
:disabled="clientsStore.isUpdating"
|
:disabled="clientsStore.isUpdating"
|
||||||
>
|
variant="outlined"
|
||||||
<option value="Active">Active</option>
|
hide-details
|
||||||
<option value="Paused">Paused</option>
|
/>
|
||||||
<option value="Archived">Archived</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="field field-wide image-field">
|
<div class="field field-wide image-field">
|
||||||
<span>Client logo</span>
|
<span>Client logo</span>
|
||||||
@@ -332,23 +329,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Primary contact name</span>
|
v-model="form.primaryContactName"
|
||||||
<input
|
label="Primary contact name"
|
||||||
v-model="form.primaryContactName"
|
:disabled="clientsStore.isUpdating"
|
||||||
type="text"
|
variant="outlined"
|
||||||
:disabled="clientsStore.isUpdating"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Primary contact email</span>
|
v-model="form.primaryContactEmail"
|
||||||
<input
|
label="Primary contact email"
|
||||||
v-model="form.primaryContactEmail"
|
:disabled="clientsStore.isUpdating"
|
||||||
type="email"
|
type="email"
|
||||||
:disabled="clientsStore.isUpdating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<div class="field field-wide image-field">
|
<div class="field field-wide image-field">
|
||||||
<span>Primary contact portrait</span>
|
<span>Primary contact portrait</span>
|
||||||
|
|||||||
@@ -101,52 +101,53 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>{{ t('clients.fields.name') }}</span>
|
v-model="form.name"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.name"
|
:label="t('clients.fields.name')"
|
||||||
type="text"
|
:disabled="clientsStore.isCreating"
|
||||||
:disabled="clientsStore.isCreating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>{{ t('clients.fields.portraitUrl') }}</span>
|
v-model="form.portraitUrl"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.portraitUrl"
|
:label="t('clients.fields.portraitUrl')"
|
||||||
type="url"
|
:disabled="clientsStore.isCreating"
|
||||||
placeholder="https://..."
|
placeholder="https://..."
|
||||||
:disabled="clientsStore.isCreating"
|
type="url"
|
||||||
/>
|
variant="outlined"
|
||||||
</label>
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('clients.fields.primaryContactName') }}</span>
|
v-model="form.primaryContactName"
|
||||||
<input
|
:label="t('clients.fields.primaryContactName')"
|
||||||
v-model="form.primaryContactName"
|
:disabled="clientsStore.isCreating"
|
||||||
type="text"
|
variant="outlined"
|
||||||
:disabled="clientsStore.isCreating"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('clients.fields.primaryContactEmail') }}</span>
|
v-model="form.primaryContactEmail"
|
||||||
<input
|
:label="t('clients.fields.primaryContactEmail')"
|
||||||
v-model="form.primaryContactEmail"
|
:disabled="clientsStore.isCreating"
|
||||||
type="email"
|
type="email"
|
||||||
:disabled="clientsStore.isCreating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>{{ t('clients.fields.primaryContactPortraitUrl') }}</span>
|
v-model="form.primaryContactPortraitUrl"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.primaryContactPortraitUrl"
|
:label="t('clients.fields.primaryContactPortraitUrl')"
|
||||||
type="url"
|
:disabled="clientsStore.isCreating"
|
||||||
placeholder="https://..."
|
placeholder="https://..."
|
||||||
:disabled="clientsStore.isCreating"
|
type="url"
|
||||||
/>
|
variant="outlined"
|
||||||
</label>
|
hide-details
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
import { mdiAt, mdiClose, mdiImagePlusOutline, mdiLockOutline, mdiSend } from '@mdi/js';
|
import { mdiAt, mdiClose, mdiImagePlusOutline, mdiLockOutline, mdiSend } from '@mdi/js';
|
||||||
import AppAvatar from '@/components/AppAvatar.vue';
|
import AppAvatar from '@/components/AppAvatar.vue';
|
||||||
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
const emit = defineEmits(['submit-comment', 'cancel-reply']);
|
const emit = defineEmits(['submit-comment', 'cancel-reply']);
|
||||||
const userProfileStore = useUserProfileStore();
|
const userProfileStore = useUserProfileStore();
|
||||||
const mediaFileInput = ref(null);
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
body: '',
|
body: '',
|
||||||
@@ -73,25 +72,15 @@
|
|||||||
form.isInternal = false;
|
form.isInternal = false;
|
||||||
form.mediaFile = null;
|
form.mediaFile = null;
|
||||||
form.showMentionPicker = false;
|
form.showMentionPicker = false;
|
||||||
if (mediaFileInput.value) {
|
|
||||||
mediaFileInput.value.value = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMediaPicker() {
|
function selectMediaFile(file) {
|
||||||
mediaFileInput.value?.click();
|
form.mediaFile = Array.isArray(file) ? file[0] ?? null : file;
|
||||||
form.showMentionPicker = false;
|
form.showMentionPicker = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMediaFile(event) {
|
|
||||||
form.mediaFile = event.target.files?.[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearMediaFile() {
|
function clearMediaFile() {
|
||||||
form.mediaFile = null;
|
form.mediaFile = null;
|
||||||
if (mediaFileInput.value) {
|
|
||||||
mediaFileInput.value.value = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMentionPicker() {
|
function toggleMentionPicker() {
|
||||||
@@ -143,11 +132,15 @@
|
|||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<textarea
|
<v-textarea
|
||||||
v-model="form.body"
|
v-model="form.body"
|
||||||
class="comment-textarea"
|
class="comment-textarea"
|
||||||
:placeholder="replyTarget ? 'Write a reply...' : 'Write a comment...'"
|
:placeholder="replyTarget ? 'Write a reply...' : 'Write a comment...'"
|
||||||
></textarea>
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
auto-grow
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -193,32 +186,27 @@
|
|||||||
|
|
||||||
<div class="comment-composer-toolbar">
|
<div class="comment-composer-toolbar">
|
||||||
<div class="comment-tool-actions">
|
<div class="comment-tool-actions">
|
||||||
<label
|
<div
|
||||||
class="icon-tool-button internal-toggle"
|
class="icon-tool-button internal-toggle"
|
||||||
:class="{ active: form.isInternal }"
|
:class="{ active: form.isInternal }"
|
||||||
title="Internal comment"
|
title="Internal comment"
|
||||||
>
|
>
|
||||||
<input
|
<v-checkbox-btn
|
||||||
v-model="form.isInternal"
|
v-model="form.isInternal"
|
||||||
type="checkbox"
|
density="compact"
|
||||||
/>
|
/>
|
||||||
<v-icon :icon="mdiLockOutline" />
|
<v-icon :icon="mdiLockOutline" />
|
||||||
</label>
|
</div>
|
||||||
<button
|
<v-file-input
|
||||||
class="icon-tool-button"
|
v-model="form.mediaFile"
|
||||||
type="button"
|
class="media-file-control"
|
||||||
title="Upload media from computer"
|
title="Upload media from computer"
|
||||||
:class="{ active: form.mediaFile }"
|
|
||||||
@click="openMediaPicker"
|
|
||||||
>
|
|
||||||
<v-icon :icon="mdiImagePlusOutline" />
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
ref="mediaFileInput"
|
|
||||||
class="sr-only"
|
|
||||||
type="file"
|
|
||||||
accept="image/png,image/jpeg,image/jpg"
|
accept="image/png,image/jpeg,image/jpg"
|
||||||
@change="selectMediaFile"
|
:prepend-icon="mdiImagePlusOutline"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="selectMediaFile"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="icon-tool-button"
|
class="icon-tool-button"
|
||||||
|
|||||||
@@ -787,40 +787,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Title</span>
|
v-model="form.title"
|
||||||
<input
|
label="Title"
|
||||||
v-model="form.title"
|
variant="outlined"
|
||||||
type="text"
|
hide-details
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
|
||||||
<span>Campaign</span>
|
|
||||||
<select v-model="form.campaignId">
|
|
||||||
<option
|
|
||||||
disabled
|
|
||||||
value=""
|
|
||||||
>
|
|
||||||
Select a campaign
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="campaign in availableCampaigns"
|
|
||||||
:key="campaign.id"
|
|
||||||
:value="campaign.id"
|
|
||||||
>
|
|
||||||
{{ campaign.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
|
||||||
<span>Due date</span>
|
|
||||||
<input
|
|
||||||
v-model="form.dueDate"
|
|
||||||
type="date"
|
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
<v-select
|
||||||
|
v-model="form.campaignId"
|
||||||
|
:items="availableCampaigns"
|
||||||
|
label="Campaign"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
placeholder="Select a campaign"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.dueDate"
|
||||||
|
label="Due date"
|
||||||
|
type="date"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="date-context field-wide">
|
<div class="date-context field-wide">
|
||||||
<div class="date-context-days">
|
<div class="date-context-days">
|
||||||
@@ -864,19 +855,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>Change summary</span>
|
v-model="form.changeSummary"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.changeSummary"
|
label="Change summary"
|
||||||
type="text"
|
placeholder="What changed in this revision?"
|
||||||
placeholder="What changed in this revision?"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-textarea
|
||||||
<span>Shared brief / base caption</span>
|
v-model="form.body"
|
||||||
<textarea v-model="form.body"></textarea>
|
class="field-wide"
|
||||||
</label>
|
label="Shared brief / base caption"
|
||||||
|
rows="4"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<label class="field field-wide">
|
||||||
<span>Shared hashtags</span>
|
<span>Shared hashtags</span>
|
||||||
@@ -899,11 +894,13 @@
|
|||||||
{{ tag.startsWith('#') ? tag : `#${tag}` }}
|
{{ tag.startsWith('#') ? tag : `#${tag}` }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<v-text-field
|
||||||
v-model="form.hashtags"
|
v-model="form.hashtags"
|
||||||
type="text"
|
|
||||||
class="hashtags-inline-input"
|
class="hashtags-inline-input"
|
||||||
placeholder="#launch #campaign #brand"
|
placeholder="#launch #campaign #brand"
|
||||||
|
density="compact"
|
||||||
|
variant="plain"
|
||||||
|
hide-details
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -961,63 +958,61 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-grid compact-grid">
|
<div class="form-grid compact-grid">
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Network</span>
|
v-model="placement.network"
|
||||||
<input
|
label="Network"
|
||||||
v-model="placement.network"
|
placeholder="Instagram, YouTube..."
|
||||||
type="text"
|
variant="outlined"
|
||||||
placeholder="Instagram, YouTube..."
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>Channel</span>
|
v-model="placement.channelId"
|
||||||
<select
|
:items="availableChannels.map(channel => ({
|
||||||
v-model="placement.channelId"
|
title: `${channel.name}${channel.network ? ` · ${channel.network}` : ''}`,
|
||||||
@change="syncPlacementChannel(placement, placement.channelId)"
|
value: channel.id,
|
||||||
>
|
}))"
|
||||||
<option value="">Select a configured channel</option>
|
label="Channel"
|
||||||
<option
|
placeholder="Select a configured channel"
|
||||||
v-for="channel in availableChannels"
|
variant="outlined"
|
||||||
:key="channel.id"
|
hide-details
|
||||||
:value="channel.id"
|
clearable
|
||||||
>
|
@update:model-value="syncPlacementChannel(placement, $event)"
|
||||||
{{ channel.name }}{{ channel.network ? ` · ${channel.network}` : '' }}
|
/>
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Channel name</span>
|
v-model="placement.channelName"
|
||||||
<input
|
label="Channel name"
|
||||||
v-model="placement.channelName"
|
placeholder="IG Feed, YouTube Main..."
|
||||||
type="text"
|
variant="outlined"
|
||||||
placeholder="IG Feed, YouTube Main..."
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Variant label</span>
|
v-model="placement.variantLabel"
|
||||||
<input
|
label="Variant label"
|
||||||
v-model="placement.variantLabel"
|
placeholder="Reel version, Shorts version..."
|
||||||
type="text"
|
variant="outlined"
|
||||||
placeholder="Reel version, Shorts version..."
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-textarea
|
||||||
<span>Channel-specific caption</span>
|
v-model="placement.message"
|
||||||
<textarea v-model="placement.message"></textarea>
|
class="field-wide"
|
||||||
</label>
|
label="Channel-specific caption"
|
||||||
|
rows="3"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>Channel-specific hashtags</span>
|
v-model="placement.hashtags"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="placement.hashtags"
|
label="Channel-specific hashtags"
|
||||||
type="text"
|
placeholder="#product #launch"
|
||||||
placeholder="#product #launch"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="media-section">
|
<div class="media-section">
|
||||||
@@ -1042,32 +1037,30 @@
|
|||||||
class="media-card"
|
class="media-card"
|
||||||
>
|
>
|
||||||
<div class="form-grid compact-grid">
|
<div class="form-grid compact-grid">
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>Media type</span>
|
v-model="media.mediaType"
|
||||||
<select v-model="media.mediaType">
|
:items="['Image', 'Video', 'Document']"
|
||||||
<option value="Image">Image</option>
|
label="Media type"
|
||||||
<option value="Video">Video</option>
|
variant="outlined"
|
||||||
<option value="Document">Document</option>
|
hide-details
|
||||||
</select>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>Label</span>
|
v-model="media.label"
|
||||||
<input
|
label="Label"
|
||||||
v-model="media.label"
|
placeholder="Cover image, YouTube video..."
|
||||||
type="text"
|
variant="outlined"
|
||||||
placeholder="Cover image, YouTube video..."
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>Media URL / reference</span>
|
v-model="media.url"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="media.url"
|
label="Media URL / reference"
|
||||||
type="text"
|
placeholder="Google Drive link or asset URL"
|
||||||
placeholder="Google Drive link or asset URL"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -1170,46 +1163,43 @@
|
|||||||
|
|
||||||
<template v-else-if="activeProductionTab === 'assets'">
|
<template v-else-if="activeProductionTab === 'assets'">
|
||||||
<div class="panel-stack asset-form">
|
<div class="panel-stack asset-form">
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>Type</span>
|
v-model="assetForm.assetType"
|
||||||
<select v-model="assetForm.assetType">
|
:items="['Image', 'Video', 'Document', 'Other']"
|
||||||
<option value="Image">Image</option>
|
label="Type"
|
||||||
<option value="Video">Video</option>
|
variant="outlined"
|
||||||
<option value="Document">Document</option>
|
hide-details
|
||||||
<option value="Other">Other</option>
|
/>
|
||||||
</select>
|
<v-text-field
|
||||||
</label>
|
v-model="assetForm.displayName"
|
||||||
<label class="field">
|
label="Name"
|
||||||
<span>Name</span>
|
placeholder="Final reel, cover image..."
|
||||||
<input
|
variant="outlined"
|
||||||
v-model="assetForm.displayName"
|
hide-details
|
||||||
type="text"
|
/>
|
||||||
placeholder="Final reel, cover image..."
|
<v-text-field
|
||||||
/>
|
v-model="assetForm.googleDriveLink"
|
||||||
</label>
|
class="field-wide"
|
||||||
<label class="field field-wide">
|
label="Google Drive link"
|
||||||
<span>Google Drive link</span>
|
type="url"
|
||||||
<input
|
placeholder="https://drive.google.com/..."
|
||||||
v-model="assetForm.googleDriveLink"
|
variant="outlined"
|
||||||
type="url"
|
hide-details
|
||||||
placeholder="https://drive.google.com/..."
|
/>
|
||||||
/>
|
<v-text-field
|
||||||
</label>
|
v-model="assetForm.googleDriveFileId"
|
||||||
<label class="field">
|
label="File id"
|
||||||
<span>File id</span>
|
placeholder="Optional if link includes it"
|
||||||
<input
|
variant="outlined"
|
||||||
v-model="assetForm.googleDriveFileId"
|
hide-details
|
||||||
type="text"
|
/>
|
||||||
placeholder="Optional if link includes it"
|
<v-text-field
|
||||||
/>
|
v-model="assetForm.previewUrl"
|
||||||
</label>
|
label="Preview URL"
|
||||||
<label class="field">
|
type="url"
|
||||||
<span>Preview URL</span>
|
variant="outlined"
|
||||||
<input
|
hide-details
|
||||||
v-model="assetForm.previewUrl"
|
/>
|
||||||
type="url"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button
|
<button
|
||||||
class="primary-button field-wide"
|
class="primary-button field-wide"
|
||||||
:disabled="detailStore.actions.asset"
|
:disabled="detailStore.actions.asset"
|
||||||
@@ -1255,22 +1245,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-stack compact-form">
|
<div class="panel-stack compact-form">
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>New revision reference</span>
|
v-model="assetRevisionForm(asset.id).sourceReference"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="assetRevisionForm(asset.id).sourceReference"
|
label="New revision reference"
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="Updated Drive link or production reference"
|
placeholder="Updated Drive link or production reference"
|
||||||
/>
|
variant="outlined"
|
||||||
</label>
|
hide-details
|
||||||
<label class="field field-wide">
|
/>
|
||||||
<span>Notes</span>
|
<v-text-field
|
||||||
<input
|
v-model="assetRevisionForm(asset.id).notes"
|
||||||
v-model="assetRevisionForm(asset.id).notes"
|
class="field-wide"
|
||||||
type="text"
|
label="Notes"
|
||||||
placeholder="What changed?"
|
placeholder="What changed?"
|
||||||
/>
|
variant="outlined"
|
||||||
</label>
|
hide-details
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="secondary-button"
|
class="secondary-button"
|
||||||
:disabled="detailStore.actions.assetRevision"
|
:disabled="detailStore.actions.assetRevision"
|
||||||
|
|||||||
@@ -1302,42 +1302,48 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scope-row">
|
<v-radio-group
|
||||||
<label
|
v-model="customCalendarForm.scope"
|
||||||
|
class="scope-row"
|
||||||
|
inline
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
<v-radio
|
||||||
v-for="option in addScopeOptions"
|
v-for="option in addScopeOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
class="scope-option"
|
:label="option.label"
|
||||||
>
|
:value="option.value"
|
||||||
<input
|
/>
|
||||||
v-model="customCalendarForm.scope"
|
</v-radio-group>
|
||||||
type="radio"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
|
||||||
<span>{{ option.label }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="activeAddMode === 'catalog'"
|
v-if="activeAddMode === 'catalog'"
|
||||||
class="catalog-panel"
|
class="catalog-panel"
|
||||||
>
|
>
|
||||||
<div class="catalog-search">
|
<div class="catalog-search">
|
||||||
<input
|
<v-text-field
|
||||||
v-model="catalogFilters.search"
|
v-model="catalogFilters.search"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
type="search"
|
type="search"
|
||||||
:placeholder="t('contentItems.calendar.searchCatalog')"
|
:placeholder="t('contentItems.calendar.searchCatalog')"
|
||||||
>
|
/>
|
||||||
<input
|
<v-text-field
|
||||||
v-model="catalogFilters.country"
|
v-model="catalogFilters.country"
|
||||||
type="text"
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
maxlength="2"
|
maxlength="2"
|
||||||
:placeholder="t('contentItems.calendar.country')"
|
:placeholder="t('contentItems.calendar.country')"
|
||||||
>
|
/>
|
||||||
<input
|
<v-text-field
|
||||||
v-model="catalogFilters.category"
|
v-model="catalogFilters.category"
|
||||||
type="text"
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
:placeholder="t('contentItems.calendar.category')"
|
:placeholder="t('contentItems.calendar.category')"
|
||||||
>
|
/>
|
||||||
<button
|
<button
|
||||||
class="text-button"
|
class="text-button"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1372,31 +1378,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
v-else
|
v-else
|
||||||
class="custom-calendar-form"
|
class="custom-calendar-form"
|
||||||
@submit.prevent="addCustomSource"
|
@submit.prevent="addCustomSource"
|
||||||
>
|
>
|
||||||
<input
|
<v-text-field
|
||||||
v-model="customCalendarForm.title"
|
v-model="customCalendarForm.title"
|
||||||
type="text"
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
:placeholder="t('contentItems.calendar.calendarName')"
|
:placeholder="t('contentItems.calendar.calendarName')"
|
||||||
>
|
/>
|
||||||
<input
|
<v-text-field
|
||||||
v-model="customCalendarForm.sourceUrl"
|
v-model="customCalendarForm.sourceUrl"
|
||||||
type="url"
|
type="url"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
:placeholder="t('contentItems.calendar.icsUrl')"
|
:placeholder="t('contentItems.calendar.icsUrl')"
|
||||||
>
|
/>
|
||||||
<div class="custom-form-row">
|
<div class="custom-form-row">
|
||||||
<input
|
<v-text-field
|
||||||
v-model="customCalendarForm.color"
|
v-model="customCalendarForm.color"
|
||||||
type="color"
|
type="color"
|
||||||
>
|
density="compact"
|
||||||
<input
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
v-model="customCalendarForm.category"
|
v-model="customCalendarForm.category"
|
||||||
type="text"
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
:placeholder="t('contentItems.calendar.category')"
|
:placeholder="t('contentItems.calendar.category')"
|
||||||
>
|
/>
|
||||||
<button
|
<button
|
||||||
class="text-button"
|
class="text-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -1405,7 +1421,7 @@
|
|||||||
<span>{{ t('contentItems.calendar.addCalendar') }}</span>
|
<span>{{ t('contentItems.calendar.addCalendar') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</v-form>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="addCalendarError || calendarStore.error"
|
v-if="addCalendarError || calendarStore.error"
|
||||||
|
|||||||
@@ -317,7 +317,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
class="comment-form"
|
class="comment-form"
|
||||||
@submit.prevent="submitComment"
|
@submit.prevent="submitComment"
|
||||||
>
|
>
|
||||||
@@ -336,7 +336,7 @@
|
|||||||
>
|
>
|
||||||
{{ feedbackStore.isCommenting ? t('feedback.review.detail.commenting') : t('feedback.review.detail.addComment') }}
|
{{ feedbackStore.isCommenting ? t('feedback.review.detail.commenting') : t('feedback.review.detail.addComment') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</v-form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -100,14 +100,16 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="filter-panel">
|
<section class="filter-panel">
|
||||||
<label class="filter-search">
|
<v-text-field
|
||||||
<v-icon :icon="mdiMagnify" />
|
v-model="feedbackStore.filters.search"
|
||||||
<input
|
:label="t('feedback.review.filters.search')"
|
||||||
v-model="feedbackStore.filters.search"
|
:prepend-inner-icon="mdiMagnify"
|
||||||
type="search"
|
density="compact"
|
||||||
:placeholder="t('feedback.review.filters.search')"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
clearable
|
||||||
|
type="search"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-select
|
<v-select
|
||||||
v-model="feedbackStore.filters.type"
|
v-model="feedbackStore.filters.type"
|
||||||
@@ -139,32 +141,40 @@
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<v-text-field
|
||||||
v-model="feedbackStore.filters.reporter"
|
v-model="feedbackStore.filters.reporter"
|
||||||
class="field"
|
:label="t('feedback.review.filters.reporter')"
|
||||||
type="text"
|
density="compact"
|
||||||
:placeholder="t('feedback.review.filters.reporter')"
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<v-text-field
|
||||||
v-model="feedbackStore.filters.workspace"
|
v-model="feedbackStore.filters.workspace"
|
||||||
class="field"
|
:label="t('feedback.review.filters.workspace')"
|
||||||
type="text"
|
density="compact"
|
||||||
:placeholder="t('feedback.review.filters.workspace')"
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<v-text-field
|
||||||
v-model="feedbackStore.filters.fromDate"
|
v-model="feedbackStore.filters.fromDate"
|
||||||
class="field"
|
:label="t('feedback.review.filters.fromDate')"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
type="date"
|
type="date"
|
||||||
:aria-label="t('feedback.review.filters.fromDate')"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<v-text-field
|
||||||
v-model="feedbackStore.filters.toDate"
|
v-model="feedbackStore.filters.toDate"
|
||||||
class="field"
|
:label="t('feedback.review.filters.toDate')"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
type="date"
|
type="date"
|
||||||
:aria-label="t('feedback.review.filters.toDate')"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-select
|
<v-select
|
||||||
|
|||||||
@@ -207,18 +207,20 @@
|
|||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<form
|
<v-form
|
||||||
v-if="organization && isEditingName"
|
v-if="organization && isEditingName"
|
||||||
class="title-edit-form"
|
class="title-edit-form"
|
||||||
@submit.prevent="submitProfile"
|
@submit.prevent="submitProfile"
|
||||||
>
|
>
|
||||||
<input
|
<v-text-field
|
||||||
v-model="profileForm.name"
|
v-model="profileForm.name"
|
||||||
type="text"
|
|
||||||
maxlength="256"
|
|
||||||
autocomplete="organization"
|
|
||||||
:aria-label="t('organizationSettings.fields.name')"
|
:aria-label="t('organizationSettings.fields.name')"
|
||||||
>
|
autocomplete="organization"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
maxlength="256"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="icon-action"
|
class="icon-action"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -238,7 +240,7 @@
|
|||||||
>
|
>
|
||||||
<v-icon :icon="mdiClose" />
|
<v-icon :icon="mdiClose" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</v-form>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="title-row"
|
class="title-row"
|
||||||
@@ -335,31 +337,26 @@
|
|||||||
v-if="activeSection.key === 'members'"
|
v-if="activeSection.key === 'members'"
|
||||||
class="table-list"
|
class="table-list"
|
||||||
>
|
>
|
||||||
<form
|
<v-form
|
||||||
class="settings-form invite-form"
|
class="settings-form invite-form"
|
||||||
@submit.prevent="submitMember"
|
@submit.prevent="submitMember"
|
||||||
>
|
>
|
||||||
<label>
|
<v-text-field
|
||||||
<span>{{ t('organizationSettings.fields.memberEmail') }}</span>
|
v-model="memberForm.email"
|
||||||
<input
|
:label="t('organizationSettings.fields.memberEmail')"
|
||||||
v-model="memberForm.email"
|
autocomplete="email"
|
||||||
type="email"
|
maxlength="256"
|
||||||
maxlength="256"
|
type="email"
|
||||||
autocomplete="email"
|
variant="outlined"
|
||||||
>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
<label>
|
<v-select
|
||||||
<span>{{ t('organizationSettings.fields.memberRole') }}</span>
|
v-model="memberForm.role"
|
||||||
<select v-model="memberForm.role">
|
:items="memberRoleOptions.map(role => ({ title: t(`organizationSettings.roles.${role}`, role), value: role }))"
|
||||||
<option
|
:label="t('organizationSettings.fields.memberRole')"
|
||||||
v-for="role in memberRoleOptions"
|
variant="outlined"
|
||||||
:key="role"
|
hide-details
|
||||||
:value="role"
|
/>
|
||||||
>
|
|
||||||
{{ t(`organizationSettings.roles.${role}`, role) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button
|
<button
|
||||||
class="primary-action"
|
class="primary-action"
|
||||||
@@ -369,7 +366,7 @@
|
|||||||
{{ organizationStore.isAddingMember ? t('organizationSettings.addingMember') : t('organizationSettings.addMember') }}
|
{{ organizationStore.isAddingMember ? t('organizationSettings.addingMember') : t('organizationSettings.addMember') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</v-form>
|
||||||
<div
|
<div
|
||||||
v-for="member in organization.members"
|
v-for="member in organization.members"
|
||||||
:key="member.userId"
|
:key="member.userId"
|
||||||
|
|||||||
@@ -206,51 +206,48 @@
|
|||||||
{{ settingsStatus }}
|
{{ settingsStatus }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
class="form-stack"
|
class="form-stack"
|
||||||
@submit.prevent="submitSettings"
|
@submit.prevent="submitSettings"
|
||||||
>
|
>
|
||||||
<div class="details-grid">
|
<div class="details-grid">
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('userSettings.firstname') }}</span>
|
v-model="form.firstname"
|
||||||
<input
|
:label="t('userSettings.firstname')"
|
||||||
v-model="form.firstname"
|
autocomplete="given-name"
|
||||||
type="text"
|
:disabled="userProfileStore.isUpdating"
|
||||||
autocomplete="given-name"
|
variant="outlined"
|
||||||
:disabled="userProfileStore.isUpdating"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('userSettings.lastname') }}</span>
|
v-model="form.lastname"
|
||||||
<input
|
:label="t('userSettings.lastname')"
|
||||||
v-model="form.lastname"
|
autocomplete="family-name"
|
||||||
type="text"
|
:disabled="userProfileStore.isUpdating"
|
||||||
autocomplete="family-name"
|
variant="outlined"
|
||||||
:disabled="userProfileStore.isUpdating"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('userSettings.alias') }}</span>
|
v-model="form.alias"
|
||||||
<input
|
:label="t('userSettings.alias')"
|
||||||
v-model="form.alias"
|
autocomplete="nickname"
|
||||||
type="text"
|
:placeholder="fullname"
|
||||||
autocomplete="nickname"
|
:disabled="userProfileStore.isUpdating"
|
||||||
:placeholder="fullname"
|
variant="outlined"
|
||||||
:disabled="userProfileStore.isUpdating"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('userSettings.email') }}</span>
|
v-model="form.email"
|
||||||
<input
|
:label="t('userSettings.email')"
|
||||||
v-model="form.email"
|
autocomplete="email"
|
||||||
type="email"
|
:disabled="userProfileStore.isUpdating"
|
||||||
autocomplete="email"
|
type="email"
|
||||||
:disabled="userProfileStore.isUpdating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
@@ -262,7 +259,7 @@
|
|||||||
{{ userProfileStore.isUpdating ? t('common.saving') : t('userSettings.saveDetails') }}
|
{{ userProfileStore.isUpdating ? t('common.saving') : t('userSettings.saveDetails') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</v-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import {
|
import {
|
||||||
mdiArrowDown,
|
mdiArrowDown,
|
||||||
mdiArrowUp,
|
mdiArrowUp,
|
||||||
@@ -40,6 +41,22 @@
|
|||||||
];
|
];
|
||||||
const membershipOptions = ['Team', 'Client'];
|
const membershipOptions = ['Team', 'Client'];
|
||||||
const targetTypes = ['Role', 'Membership', 'Member'];
|
const targetTypes = ['Role', 'Membership', 'Member'];
|
||||||
|
const roleItems = computed(() => roleOptions.map(role => ({
|
||||||
|
title: props.labels.roles[role],
|
||||||
|
value: role,
|
||||||
|
})));
|
||||||
|
const membershipItems = computed(() => membershipOptions.map(membership => ({
|
||||||
|
title: props.labels.memberships[membership],
|
||||||
|
value: membership,
|
||||||
|
})));
|
||||||
|
const targetTypeItems = computed(() => targetTypes.map(targetType => ({
|
||||||
|
title: props.labels.targetTypes[targetType],
|
||||||
|
value: targetType,
|
||||||
|
})));
|
||||||
|
const memberItems = computed(() => props.members.map(member => ({
|
||||||
|
title: `${member.displayName} - ${member.email}`,
|
||||||
|
value: member.id,
|
||||||
|
})));
|
||||||
|
|
||||||
function emitSteps(steps) {
|
function emitSteps(steps) {
|
||||||
emit('update:modelValue', steps.map((step, index) => ({
|
emit('update:modelValue', steps.map((step, index) => ({
|
||||||
@@ -101,13 +118,8 @@
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMemberTargets(index, selectedOptions) {
|
function updateMemberTargets(index, selectedMemberIds) {
|
||||||
const targetValue = Array.from(selectedOptions)
|
updateStep(index, { targetValue: selectedMemberIds.filter(Boolean).join(',') });
|
||||||
.map(option => option.value)
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(',');
|
|
||||||
|
|
||||||
updateStep(index, { targetValue });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveStep(index, offset) {
|
function moveStep(index, offset) {
|
||||||
@@ -198,13 +210,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="approval-step-fields">
|
<div class="approval-step-fields">
|
||||||
<label class="field">
|
<div class="field">
|
||||||
<span>{{ labels.fields.name }}</span>
|
<v-text-field
|
||||||
<input
|
:model-value="step.name"
|
||||||
:value="step.name"
|
:label="labels.fields.name"
|
||||||
type="text"
|
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@input="updateStep(index, { name: $event.target.value })"
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="updateStep(index, { name: $event })"
|
||||||
/>
|
/>
|
||||||
<small
|
<small
|
||||||
v-if="errors[index]?.name"
|
v-if="errors[index]?.name"
|
||||||
@@ -212,73 +225,54 @@
|
|||||||
>
|
>
|
||||||
{{ errors[index].name }}
|
{{ errors[index].name }}
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>{{ labels.fields.targetType }}</span>
|
:model-value="step.targetType"
|
||||||
<select
|
:items="targetTypeItems"
|
||||||
:value="step.targetType"
|
:label="labels.fields.targetType"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@change="updateStep(index, { targetType: $event.target.value })"
|
variant="outlined"
|
||||||
>
|
hide-details
|
||||||
<option
|
@update:model-value="updateStep(index, { targetType: $event })"
|
||||||
v-for="targetType in targetTypes"
|
/>
|
||||||
:key="targetType"
|
|
||||||
:value="targetType"
|
|
||||||
>
|
|
||||||
{{ labels.targetTypes[targetType] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<div class="field">
|
||||||
<span>{{ labels.fields.targetValue }}</span>
|
<v-select
|
||||||
<select
|
|
||||||
v-if="step.targetType === 'Role'"
|
v-if="step.targetType === 'Role'"
|
||||||
:value="step.targetValue"
|
:model-value="step.targetValue"
|
||||||
|
:items="roleItems"
|
||||||
|
:label="labels.fields.targetValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@change="updateStep(index, { targetValue: $event.target.value })"
|
variant="outlined"
|
||||||
>
|
hide-details
|
||||||
<option
|
@update:model-value="updateStep(index, { targetValue: $event })"
|
||||||
v-for="role in roleOptions"
|
/>
|
||||||
:key="role"
|
|
||||||
:value="role"
|
|
||||||
>
|
|
||||||
{{ labels.roles[role] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
<v-select
|
||||||
v-else-if="step.targetType === 'Membership'"
|
v-else-if="step.targetType === 'Membership'"
|
||||||
:value="step.targetValue"
|
:model-value="step.targetValue"
|
||||||
|
:items="membershipItems"
|
||||||
|
:label="labels.fields.targetValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@change="updateStep(index, { targetValue: $event.target.value })"
|
variant="outlined"
|
||||||
>
|
hide-details
|
||||||
<option
|
@update:model-value="updateStep(index, { targetValue: $event })"
|
||||||
v-for="membership in membershipOptions"
|
/>
|
||||||
:key="membership"
|
|
||||||
:value="membership"
|
|
||||||
>
|
|
||||||
{{ labels.memberships[membership] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
<v-select
|
||||||
v-else
|
v-else
|
||||||
:value="getSelectedMemberIds(step)"
|
:model-value="getSelectedMemberIds(step)"
|
||||||
|
:items="memberItems"
|
||||||
|
:label="labels.fields.targetValue"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
multiple
|
multiple
|
||||||
size="5"
|
chips
|
||||||
@change="updateMemberTargets(index, $event.target.selectedOptions)"
|
closable-chips
|
||||||
>
|
variant="outlined"
|
||||||
<option
|
hide-details
|
||||||
v-for="member in members"
|
@update:model-value="updateMemberTargets(index, $event)"
|
||||||
:key="member.id"
|
/>
|
||||||
:value="member.id"
|
|
||||||
>
|
|
||||||
{{ member.displayName }} - {{ member.email }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<small
|
<small
|
||||||
v-if="step.targetType === 'Member'"
|
v-if="step.targetType === 'Member'"
|
||||||
class="field-help"
|
class="field-help"
|
||||||
@@ -292,17 +286,19 @@
|
|||||||
>
|
>
|
||||||
{{ errors[index].targetValue }}
|
{{ errors[index].targetValue }}
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<div class="field">
|
||||||
<span>{{ labels.fields.requiredApproverCount }}</span>
|
<v-text-field
|
||||||
<input
|
:model-value="step.requiredApproverCount"
|
||||||
:value="step.requiredApproverCount"
|
:label="labels.fields.requiredApproverCount"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
step="1"
|
step="1"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@input="updateStep(index, { requiredApproverCount: Number($event.target.value) })"
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="updateStep(index, { requiredApproverCount: Number($event) })"
|
||||||
/>
|
/>
|
||||||
<small
|
<small
|
||||||
v-if="errors[index]?.requiredApproverCount"
|
v-if="errors[index]?.requiredApproverCount"
|
||||||
@@ -310,7 +306,7 @@
|
|||||||
>
|
>
|
||||||
{{ errors[index].requiredApproverCount }}
|
{{ errors[index].requiredApproverCount }}
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,38 +13,23 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
|
||||||
const timeZoneOptions = computed(() => getTimeZoneOptions(props.modelValue));
|
const timeZoneOptions = computed(() => getTimeZoneOptions(props.modelValue));
|
||||||
|
|
||||||
function updateValue(event) {
|
|
||||||
emit('update:modelValue', event.target.value);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<select
|
<v-select
|
||||||
class="time-zone-select"
|
:model-value="modelValue"
|
||||||
:value="modelValue"
|
:items="timeZoneOptions"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@change="updateValue"
|
item-title="label"
|
||||||
>
|
item-value="value"
|
||||||
<option
|
density="compact"
|
||||||
v-for="timeZone in timeZoneOptions"
|
variant="outlined"
|
||||||
:key="timeZone.value"
|
hide-details
|
||||||
:value="timeZone.value"
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
>
|
/>
|
||||||
{{ timeZone.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "@/assets/main.css";
|
@reference "@/assets/main.css";
|
||||||
.time-zone-select {
|
|
||||||
@apply rounded-[1rem] border px-4 py-3 text-sm;
|
|
||||||
background: #fffdf8;
|
|
||||||
border-color: rgba(23, 32, 51, 0.1);
|
|
||||||
color: #172033;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -82,43 +82,35 @@
|
|||||||
{{ formError }}
|
{{ formError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
class="form-grid"
|
class="form-grid"
|
||||||
@submit.prevent="submitForm"
|
@submit.prevent="submitForm"
|
||||||
>
|
>
|
||||||
<label class="field field-wide">
|
<v-text-field
|
||||||
<span>{{ t('workspaceCreate.fields.name') }}</span>
|
v-model="form.name"
|
||||||
<input
|
class="field-wide"
|
||||||
v-model="form.name"
|
:label="t('workspaceCreate.fields.name')"
|
||||||
type="text"
|
:placeholder="t('workspaceCreate.fields.namePlaceholder')"
|
||||||
:placeholder="t('workspaceCreate.fields.namePlaceholder')"
|
:disabled="workspaceStore.isCreating"
|
||||||
:disabled="workspaceStore.isCreating"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>{{ t('workspaceCreate.fields.organization') }}</span>
|
v-model="selectedOrganizationId"
|
||||||
<select
|
:items="organizationStore.organizations"
|
||||||
v-model="selectedOrganizationId"
|
:label="t('workspaceCreate.fields.organization')"
|
||||||
:disabled="workspaceStore.isCreating || organizationStore.organizations.length <= 1"
|
:disabled="workspaceStore.isCreating || organizationStore.organizations.length <= 1"
|
||||||
>
|
item-title="name"
|
||||||
<option
|
item-value="id"
|
||||||
v-for="organization in organizationStore.organizations"
|
variant="outlined"
|
||||||
:key="organization.id"
|
hide-details
|
||||||
:value="organization.id"
|
/>
|
||||||
>
|
|
||||||
{{ organization.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<TimeZoneSelect
|
||||||
<span>{{ t('workspaceCreate.fields.timeZone') }}</span>
|
v-model="form.timeZone"
|
||||||
<TimeZoneSelect
|
:disabled="workspaceStore.isCreating"
|
||||||
v-model="form.timeZone"
|
/>
|
||||||
:disabled="workspaceStore.isCreating"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="panel-actions field-wide">
|
<div class="panel-actions field-wide">
|
||||||
<button
|
<button
|
||||||
@@ -137,7 +129,7 @@
|
|||||||
{{ workspaceStore.isCreating ? t('common.creating') : t('workspaceCreate.createAction') }}
|
{{ workspaceStore.isCreating ? t('common.creating') : t('workspaceCreate.createAction') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</v-form>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -460,7 +460,7 @@
|
|||||||
{{ settingsStatus }}
|
{{ settingsStatus }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
class="form-stack"
|
class="form-stack"
|
||||||
@submit.prevent="submitWorkspaceSettings"
|
@submit.prevent="submitWorkspaceSettings"
|
||||||
>
|
>
|
||||||
@@ -496,14 +496,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('workspaceSettings.fields.name') }}</span>
|
v-model="settingsForm.name"
|
||||||
<input
|
:label="t('workspaceSettings.fields.name')"
|
||||||
v-model="settingsForm.name"
|
:disabled="workspaceStore.isUpdating"
|
||||||
type="text"
|
variant="outlined"
|
||||||
:disabled="workspaceStore.isUpdating"
|
hide-details
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>{{ t('workspaceSettings.fields.timeZone') }}</span>
|
<span>{{ t('workspaceSettings.fields.timeZone') }}</span>
|
||||||
@@ -520,7 +519,7 @@
|
|||||||
>
|
>
|
||||||
{{ workspaceStore.isUpdating ? t('common.saving') : t('workspaceSettings.general.saveAction') }}
|
{{ workspaceStore.isUpdating ? t('common.saving') : t('workspaceSettings.general.saveAction') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</v-form>
|
||||||
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -535,26 +534,29 @@
|
|||||||
<p>{{ t('workspaceSettings.inviteDescription') }}</p>
|
<p>{{ t('workspaceSettings.inviteDescription') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<v-form
|
||||||
class="form-stack"
|
class="form-stack"
|
||||||
@submit.prevent="submitInvite"
|
@submit.prevent="submitInvite"
|
||||||
>
|
>
|
||||||
<label class="field">
|
<v-text-field
|
||||||
<span>{{ t('workspaceSettings.fields.memberEmail') }}</span>
|
v-model="inviteForm.email"
|
||||||
<input
|
:label="t('workspaceSettings.fields.memberEmail')"
|
||||||
v-model="inviteForm.email"
|
type="email"
|
||||||
type="email"
|
variant="outlined"
|
||||||
/>
|
hide-details
|
||||||
</label>
|
/>
|
||||||
|
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>{{ t('workspaceSettings.fields.memberRole') }}</span>
|
v-model="inviteForm.role"
|
||||||
<select v-model="inviteForm.role">
|
:items="[
|
||||||
<option value="workspace-member">{{ t('workspaceSettings.roles.workspace-member') }}</option>
|
{ title: t('workspaceSettings.roles.workspace-member'), value: 'workspace-member' },
|
||||||
<option value="client">{{ t('workspaceSettings.roles.client') }}</option>
|
{ title: t('workspaceSettings.roles.client'), value: 'client' },
|
||||||
<option value="provider">{{ t('workspaceSettings.roles.provider') }}</option>
|
{ title: t('workspaceSettings.roles.provider'), value: 'provider' },
|
||||||
</select>
|
]"
|
||||||
</label>
|
:label="t('workspaceSettings.fields.memberRole')"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary-button"
|
class="primary-button"
|
||||||
@@ -562,7 +564,7 @@
|
|||||||
>
|
>
|
||||||
{{ workspaceStore.isInviting ? t('common.creating') : t('workspaceSettings.sendInvite') }}
|
{{ workspaceStore.isInviting ? t('common.creating') : t('workspaceSettings.sendInvite') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</v-form>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="settings-card">
|
<article class="settings-card">
|
||||||
@@ -667,21 +669,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="workflow-rule-list">
|
<div class="workflow-rule-list">
|
||||||
<label class="field">
|
<v-select
|
||||||
<span>{{ t('workspaceSettings.approvals.fields.approvalMode') }}</span>
|
v-model="settingsForm.approvalMode"
|
||||||
<select
|
:items="approvalModeOptions"
|
||||||
v-model="settingsForm.approvalMode"
|
:label="t('workspaceSettings.approvals.fields.approvalMode')"
|
||||||
:disabled="workspaceStore.isUpdating"
|
:disabled="workspaceStore.isUpdating"
|
||||||
>
|
item-title="label"
|
||||||
<option
|
item-value="value"
|
||||||
v-for="option in approvalModeOptions"
|
variant="outlined"
|
||||||
:key="option.value"
|
hide-details
|
||||||
:value="option.value"
|
/>
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="workflow-rule">
|
<div class="workflow-rule">
|
||||||
<strong>{{ activeApprovalModeOption.label }}</strong>
|
<strong>{{ activeApprovalModeOption.label }}</strong>
|
||||||
@@ -697,41 +694,44 @@
|
|||||||
:labels="approvalWorkflowEditorLabels"
|
:labels="approvalWorkflowEditorLabels"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label class="workflow-toggle">
|
<div class="workflow-toggle">
|
||||||
<input
|
<v-checkbox
|
||||||
v-model="settingsForm.schedulePostsAutomaticallyOnApproval"
|
v-model="settingsForm.schedulePostsAutomaticallyOnApproval"
|
||||||
type="checkbox"
|
|
||||||
:disabled="workspaceStore.isUpdating"
|
:disabled="workspaceStore.isUpdating"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ t('workspaceSettings.approvals.fields.schedulePostsAutomaticallyOnApproval') }}</strong>
|
<strong>{{ t('workspaceSettings.approvals.fields.schedulePostsAutomaticallyOnApproval') }}</strong>
|
||||||
<small>{{ t('workspaceSettings.approvals.fieldHelp.schedulePostsAutomaticallyOnApproval') }}</small>
|
<small>{{ t('workspaceSettings.approvals.fieldHelp.schedulePostsAutomaticallyOnApproval') }}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<label class="workflow-toggle">
|
<div class="workflow-toggle">
|
||||||
<input
|
<v-checkbox
|
||||||
v-model="settingsForm.lockContentAfterApproval"
|
v-model="settingsForm.lockContentAfterApproval"
|
||||||
type="checkbox"
|
|
||||||
:disabled="workspaceStore.isUpdating"
|
:disabled="workspaceStore.isUpdating"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ t('workspaceSettings.approvals.fields.lockContentAfterApproval') }}</strong>
|
<strong>{{ t('workspaceSettings.approvals.fields.lockContentAfterApproval') }}</strong>
|
||||||
<small>{{ t('workspaceSettings.approvals.fieldHelp.lockContentAfterApproval') }}</small>
|
<small>{{ t('workspaceSettings.approvals.fieldHelp.lockContentAfterApproval') }}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<label class="workflow-toggle">
|
<div class="workflow-toggle">
|
||||||
<input
|
<v-checkbox
|
||||||
v-model="settingsForm.sendAutomaticApprovalReminders"
|
v-model="settingsForm.sendAutomaticApprovalReminders"
|
||||||
type="checkbox"
|
|
||||||
:disabled="workspaceStore.isUpdating"
|
:disabled="workspaceStore.isUpdating"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ t('workspaceSettings.approvals.fields.sendAutomaticApprovalReminders') }}</strong>
|
<strong>{{ t('workspaceSettings.approvals.fields.sendAutomaticApprovalReminders') }}</strong>
|
||||||
<small>{{ t('workspaceSettings.approvals.fieldHelp.sendAutomaticApprovalReminders') }}</small>
|
<small>{{ t('workspaceSettings.approvals.fieldHelp.sendAutomaticApprovalReminders') }}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary-button"
|
class="primary-button"
|
||||||
|
|||||||
@@ -284,25 +284,25 @@
|
|||||||
ref="searchRef"
|
ref="searchRef"
|
||||||
class="sidebar-search-wrap"
|
class="sidebar-search-wrap"
|
||||||
>
|
>
|
||||||
<label
|
<div
|
||||||
class="sidebar-search"
|
class="sidebar-search"
|
||||||
:class="{ 'sidebar-search-open': isSearchOpen }"
|
:class="{ 'sidebar-search-open': isSearchOpen }"
|
||||||
:title="!isExpanded ? 'Search' : null"
|
:title="!isExpanded ? 'Search' : null"
|
||||||
@click="openCollapsedSearch"
|
@click="openCollapsedSearch"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-text-field
|
||||||
:icon="mdiMagnify"
|
|
||||||
class="sidebar-search-icon"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-if="isExpanded"
|
v-if="isExpanded"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="search"
|
|
||||||
class="sidebar-search-input"
|
class="sidebar-search-input"
|
||||||
|
:prepend-inner-icon="mdiMagnify"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
density="compact"
|
||||||
|
variant="plain"
|
||||||
|
hide-details
|
||||||
|
type="search"
|
||||||
@focus="isSearchFocused = true"
|
@focus="isSearchFocused = true"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isSearchPanelOpen"
|
v-if="isSearchPanelOpen"
|
||||||
@@ -310,22 +310,22 @@
|
|||||||
:class="{ 'sidebar-search-panel-collapsed': !isExpanded }"
|
:class="{ 'sidebar-search-panel-collapsed': !isExpanded }"
|
||||||
:style="!isExpanded ? collapsedSearchPanelStyle : null"
|
:style="!isExpanded ? collapsedSearchPanelStyle : null"
|
||||||
>
|
>
|
||||||
<label
|
<div
|
||||||
v-if="!isExpanded"
|
v-if="!isExpanded"
|
||||||
class="sidebar-search sidebar-search-panel-input"
|
class="sidebar-search sidebar-search-panel-input"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-text-field
|
||||||
:icon="mdiMagnify"
|
|
||||||
class="sidebar-search-icon"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref="collapsedSearchInputRef"
|
ref="collapsedSearchInputRef"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="search"
|
|
||||||
class="sidebar-search-input"
|
class="sidebar-search-input"
|
||||||
|
:prepend-inner-icon="mdiMagnify"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
density="compact"
|
||||||
|
variant="plain"
|
||||||
|
hide-details
|
||||||
|
type="search"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="campaignResults.length"
|
v-if="campaignResults.length"
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ import {
|
|||||||
VAlert,
|
VAlert,
|
||||||
VApp,
|
VApp,
|
||||||
VBtn,
|
VBtn,
|
||||||
|
VCheckbox,
|
||||||
|
VCheckboxBtn,
|
||||||
VDialog,
|
VDialog,
|
||||||
|
VFileInput,
|
||||||
VForm,
|
VForm,
|
||||||
VIcon,
|
VIcon,
|
||||||
VProgressCircular,
|
VProgressCircular,
|
||||||
VProgressLinear,
|
VProgressLinear,
|
||||||
|
VRadio,
|
||||||
|
VRadioGroup,
|
||||||
VSelect,
|
VSelect,
|
||||||
VSnackbar,
|
VSnackbar,
|
||||||
VTextarea,
|
VTextarea,
|
||||||
@@ -42,9 +47,14 @@ const vuetify = createVuetify({
|
|||||||
VDialog,
|
VDialog,
|
||||||
VApp,
|
VApp,
|
||||||
VBtn,
|
VBtn,
|
||||||
|
VCheckbox,
|
||||||
|
VCheckboxBtn,
|
||||||
|
VFileInput,
|
||||||
VProgressLinear,
|
VProgressLinear,
|
||||||
VProgressCircular,
|
VProgressCircular,
|
||||||
VIcon,
|
VIcon,
|
||||||
|
VRadio,
|
||||||
|
VRadioGroup,
|
||||||
VSelect,
|
VSelect,
|
||||||
VTextField,
|
VTextField,
|
||||||
VSnackbar,
|
VSnackbar,
|
||||||
|
|||||||
Reference in New Issue
Block a user