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