mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
script[setup]: Remaining private components (#18458)
* script[setup]: Remaining private components * Remove redundant default * Remove redundant default * Remove redudant default * Remove redundant default
This commit is contained in:
@@ -15,58 +15,44 @@
|
||||
</sidebar-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, unref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, defineComponent, watch, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
archive: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
const props = defineProps<{
|
||||
collection?: string;
|
||||
archive?: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const selectedItem = ref<string | undefined>(props.archive);
|
||||
|
||||
const items = [
|
||||
{
|
||||
text: t('show_active_items'),
|
||||
value: null,
|
||||
},
|
||||
setup(props) {
|
||||
const { t } = useI18n();
|
||||
{ text: t('show_archived_items'), value: 'archived' },
|
||||
{ text: t('show_all_items'), value: 'all' },
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
const active = computed(() => !!unref(selectedItem));
|
||||
|
||||
const selectedItem = ref<string | null>(props.archive);
|
||||
watch(selectedItem, () => {
|
||||
const url = new URL(unref(router.currentRoute).fullPath, window.location.origin);
|
||||
|
||||
const items = [
|
||||
{
|
||||
text: t('show_active_items'),
|
||||
value: null,
|
||||
},
|
||||
{ text: t('show_archived_items'), value: 'archived' },
|
||||
{ text: t('show_all_items'), value: 'all' },
|
||||
];
|
||||
url.searchParams.delete('archived');
|
||||
url.searchParams.delete('all');
|
||||
|
||||
const active = computed(() => selectedItem.value !== null);
|
||||
if (unref(selectedItem)) {
|
||||
url.searchParams.set(selectedItem.value!, '');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedItem.value,
|
||||
() => {
|
||||
const url = new URL(router.currentRoute.value.fullPath, window.location.origin);
|
||||
|
||||
url.searchParams.delete('archived');
|
||||
url.searchParams.delete('all');
|
||||
|
||||
if (selectedItem.value !== null) {
|
||||
url.searchParams.set(selectedItem.value, '');
|
||||
}
|
||||
|
||||
router.push(url.pathname + url.search);
|
||||
}
|
||||
);
|
||||
|
||||
return { t, active, selectedItem, items };
|
||||
},
|
||||
router.push(url.pathname + url.search);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -35,49 +35,42 @@
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, reactive } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
saving: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['save', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
defineProps<{
|
||||
modelValue?: boolean;
|
||||
saving?: boolean;
|
||||
}>();
|
||||
|
||||
const bookmarkValue = reactive({
|
||||
name: null,
|
||||
icon: 'bookmark',
|
||||
color: null,
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', value: { name: string | null; icon: string | null; color: string | null }): void;
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
}>();
|
||||
|
||||
return { t, bookmarkValue, setIcon, setColor, cancel };
|
||||
const { t } = useI18n();
|
||||
|
||||
function setIcon(icon: any) {
|
||||
bookmarkValue.icon = icon;
|
||||
}
|
||||
|
||||
function setColor(color: any) {
|
||||
bookmarkValue.color = color;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
bookmarkValue.name = null;
|
||||
bookmarkValue.icon = 'bookmark';
|
||||
bookmarkValue.color = null;
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
},
|
||||
const bookmarkValue = reactive({
|
||||
name: null,
|
||||
icon: 'bookmark',
|
||||
color: null,
|
||||
});
|
||||
|
||||
function setIcon(icon: any) {
|
||||
bookmarkValue.icon = icon;
|
||||
}
|
||||
|
||||
function setColor(color: any) {
|
||||
bookmarkValue.color = color;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
bookmarkValue.name = null;
|
||||
bookmarkValue.icon = 'bookmark';
|
||||
bookmarkValue.color = null;
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -23,129 +23,116 @@
|
||||
</v-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, computed, PropType, toRefs } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api';
|
||||
import { VALIDATION_TYPES } from '@/constants';
|
||||
import { APIError } from '@/types/error';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { getEndpoint } from '@directus/utils';
|
||||
import { computed, ref, toRefs } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
edits: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: undefined,
|
||||
},
|
||||
primaryKeys: {
|
||||
type: Array as PropType<(number | string)[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:active', 'refresh'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
collection: string;
|
||||
primaryKeys: (number | string)[];
|
||||
active?: boolean;
|
||||
edits?: Record<string, any>;
|
||||
}>();
|
||||
|
||||
const { internalEdits } = useEdits();
|
||||
const { internalActive } = useActiveState();
|
||||
const { save, cancel, saving, validationErrors } = useActions();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:active', value: boolean): void;
|
||||
(e: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
const { t } = useI18n();
|
||||
|
||||
return { t, internalActive, internalEdits, save, saving, cancel, validationErrors };
|
||||
const { internalEdits } = useEdits();
|
||||
const { internalActive } = useActiveState();
|
||||
const { save, cancel, saving, validationErrors } = useActions();
|
||||
|
||||
function useEdits() {
|
||||
const localEdits = ref<Record<string, any>>({});
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const internalEdits = computed<Record<string, any>>({
|
||||
get() {
|
||||
if (props.edits !== undefined) {
|
||||
return {
|
||||
...props.edits,
|
||||
...localEdits.value,
|
||||
};
|
||||
}
|
||||
function useEdits() {
|
||||
const localEdits = ref<Record<string, any>>({});
|
||||
|
||||
return localEdits.value;
|
||||
},
|
||||
set(newEdits) {
|
||||
localEdits.value = newEdits;
|
||||
},
|
||||
});
|
||||
|
||||
return { internalEdits };
|
||||
}
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const internalActive = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { internalActive };
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
const saving = ref(false);
|
||||
const validationErrors = ref([]);
|
||||
|
||||
return { save, cancel, saving, validationErrors };
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.patch(getEndpoint(collection.value), {
|
||||
keys: props.primaryKeys,
|
||||
data: internalEdits.value,
|
||||
});
|
||||
|
||||
emit('refresh');
|
||||
|
||||
internalActive.value = false;
|
||||
internalEdits.value = {};
|
||||
} catch (err: any) {
|
||||
validationErrors.value = err.response.data.errors
|
||||
.filter((err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code))
|
||||
.map((err: APIError) => {
|
||||
return err.extensions;
|
||||
});
|
||||
|
||||
const otherErrors = err.response.data.errors.filter(
|
||||
(err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code) === false
|
||||
);
|
||||
|
||||
if (otherErrors.length > 0) {
|
||||
otherErrors.forEach(unexpectedError);
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
const internalEdits = computed<Record<string, any>>({
|
||||
get() {
|
||||
if (props.edits !== undefined) {
|
||||
return {
|
||||
...props.edits,
|
||||
...localEdits.value,
|
||||
};
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
internalActive.value = false;
|
||||
internalEdits.value = {};
|
||||
return localEdits.value;
|
||||
},
|
||||
set(newEdits) {
|
||||
localEdits.value = newEdits;
|
||||
},
|
||||
});
|
||||
|
||||
return { internalEdits };
|
||||
}
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const internalActive = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { internalActive };
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
const saving = ref(false);
|
||||
const validationErrors = ref([]);
|
||||
|
||||
return { save, cancel, saving, validationErrors };
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await api.patch(getEndpoint(collection.value), {
|
||||
keys: props.primaryKeys,
|
||||
data: internalEdits.value,
|
||||
});
|
||||
|
||||
emit('refresh');
|
||||
|
||||
internalActive.value = false;
|
||||
internalEdits.value = {};
|
||||
} catch (err: any) {
|
||||
validationErrors.value = err.response.data.errors
|
||||
.filter((err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code))
|
||||
.map((err: APIError) => {
|
||||
return err.extensions;
|
||||
});
|
||||
|
||||
const otherErrors = err.response.data.errors.filter(
|
||||
(err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code) === false
|
||||
);
|
||||
|
||||
if (otherErrors.length > 0) {
|
||||
otherErrors.forEach(unexpectedError);
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
internalActive.value = false;
|
||||
internalEdits.value = {};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -53,171 +53,144 @@
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, PropType, ref, computed, toRefs, watch } from 'vue';
|
||||
import { Filter } from '@directus/types';
|
||||
import { usePreset } from '@/composables/use-preset';
|
||||
import { useCollection, useLayout } from '@directus/composables';
|
||||
import SearchInput from '@/views/private/components/search-input.vue';
|
||||
<script lang="ts" setup>
|
||||
import { useExtension } from '@/composables/use-extension';
|
||||
import { usePreset } from '@/composables/use-preset';
|
||||
import SearchInput from '@/views/private/components/search-input.vue';
|
||||
import { useCollection, useLayout } from '@directus/composables';
|
||||
import { Filter } from '@directus/types';
|
||||
import { computed, ref, toRefs, unref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchInput },
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selection: {
|
||||
type: Array as PropType<(number | string)[]>,
|
||||
default: () => [],
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
filter: {
|
||||
type: Object as PropType<Filter>,
|
||||
default: null,
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
active?: boolean;
|
||||
selection?: (number | string)[];
|
||||
collection: string;
|
||||
multiple?: boolean;
|
||||
filter?: Filter;
|
||||
}>(),
|
||||
{
|
||||
selection: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:active', value: boolean): void;
|
||||
(e: 'input', value: (number | string)[] | null): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { save, cancel } = useActions();
|
||||
const { internalActive } = useActiveState();
|
||||
const { internalSelection, onSelect } = useSelection();
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
const { layout, layoutOptions, layoutQuery, search, filter: presetFilter } = usePreset(collection, ref(null), true);
|
||||
|
||||
// This is a local copy of the layout. This means that we can sync it the layout without
|
||||
// having use-preset auto-save the values
|
||||
const localLayout = ref(layout.value || 'tabular');
|
||||
const localOptions = ref(layoutOptions.value);
|
||||
const localQuery = ref(layoutQuery.value);
|
||||
|
||||
const currentLayout = useExtension('layout', localLayout);
|
||||
|
||||
const layoutSelection = computed<any>({
|
||||
get() {
|
||||
return internalSelection.value;
|
||||
},
|
||||
emits: ['update:active', 'input'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const { save, cancel } = useActions();
|
||||
const { internalActive } = useActiveState();
|
||||
const { internalSelection, onSelect } = useSelection();
|
||||
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
const { layout, layoutOptions, layoutQuery, search, filter: presetFilter } = usePreset(collection, ref(null), true);
|
||||
|
||||
// This is a local copy of the layout. This means that we can sync it the layout without
|
||||
// having use-preset auto-save the values
|
||||
const localLayout = ref(layout.value || 'tabular');
|
||||
const localOptions = ref(layoutOptions.value);
|
||||
const localQuery = ref(layoutQuery.value);
|
||||
|
||||
const currentLayout = useExtension('layout', localLayout);
|
||||
|
||||
const layoutSelection = computed<any>({
|
||||
get() {
|
||||
return internalSelection.value;
|
||||
},
|
||||
set(newFilters) {
|
||||
onSelect(newFilters);
|
||||
},
|
||||
});
|
||||
|
||||
const { layoutWrapper } = useLayout(layout);
|
||||
|
||||
const layoutFilter = computed({
|
||||
get() {
|
||||
if (!props.filter) return presetFilter.value;
|
||||
if (!presetFilter.value) return props.filter;
|
||||
|
||||
return {
|
||||
_and: [props.filter, presetFilter.value],
|
||||
};
|
||||
},
|
||||
set(newFilter: Filter | null) {
|
||||
presetFilter.value = newFilter;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
save,
|
||||
cancel,
|
||||
internalActive,
|
||||
layoutWrapper,
|
||||
layoutSelection,
|
||||
layoutFilter,
|
||||
localLayout,
|
||||
localOptions,
|
||||
localQuery,
|
||||
collectionInfo,
|
||||
search,
|
||||
presetFilter,
|
||||
currentLayout,
|
||||
};
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const internalActive = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { internalActive };
|
||||
}
|
||||
|
||||
function useSelection() {
|
||||
const localSelection = ref<(string | number)[] | null>(null);
|
||||
|
||||
const internalSelection = computed({
|
||||
get() {
|
||||
if (localSelection.value === null) {
|
||||
return props.selection;
|
||||
}
|
||||
|
||||
return localSelection.value;
|
||||
},
|
||||
set(newSelection: (string | number)[]) {
|
||||
localSelection.value = newSelection;
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => {
|
||||
localSelection.value = null;
|
||||
}
|
||||
);
|
||||
|
||||
return { internalSelection, onSelect };
|
||||
|
||||
function onSelect(newSelection: (string | number)[]) {
|
||||
if (newSelection.length === 0) {
|
||||
localSelection.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.multiple === true) {
|
||||
localSelection.value = newSelection;
|
||||
} else {
|
||||
localSelection.value = [newSelection[newSelection.length - 1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
return { save, cancel };
|
||||
|
||||
function save() {
|
||||
emit('input', internalSelection.value);
|
||||
internalActive.value = false;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
internalActive.value = false;
|
||||
}
|
||||
}
|
||||
set(newFilters) {
|
||||
onSelect(newFilters);
|
||||
},
|
||||
});
|
||||
|
||||
const { layoutWrapper } = useLayout(layout);
|
||||
|
||||
const layoutFilter = computed({
|
||||
get() {
|
||||
if (!props.filter) return presetFilter.value;
|
||||
if (!presetFilter.value) return props.filter;
|
||||
|
||||
return {
|
||||
_and: [props.filter, presetFilter.value],
|
||||
};
|
||||
},
|
||||
set(newFilter: Filter | null) {
|
||||
presetFilter.value = newFilter;
|
||||
},
|
||||
});
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
const internalActive = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:active', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
return { internalActive };
|
||||
}
|
||||
|
||||
function useSelection() {
|
||||
const localSelection = ref<(string | number)[] | null>(null);
|
||||
|
||||
const internalSelection = computed({
|
||||
get() {
|
||||
if (localSelection.value === null) {
|
||||
return props.selection;
|
||||
}
|
||||
|
||||
return localSelection.value;
|
||||
},
|
||||
set(newSelection: (string | number)[]) {
|
||||
localSelection.value = newSelection;
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => {
|
||||
localSelection.value = null;
|
||||
}
|
||||
);
|
||||
|
||||
return { internalSelection, onSelect };
|
||||
|
||||
function onSelect(newSelection: (string | number)[]) {
|
||||
if (newSelection.length === 0) {
|
||||
localSelection.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.multiple === true) {
|
||||
localSelection.value = newSelection;
|
||||
} else {
|
||||
localSelection.value = [newSelection[newSelection.length - 1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useActions() {
|
||||
return { save, cancel };
|
||||
|
||||
function save() {
|
||||
emit('input', unref(internalSelection));
|
||||
internalActive.value = false;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
internalActive.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -22,22 +22,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
showSidebarToggle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['toggle:sidebar'],
|
||||
setup() {
|
||||
const active = ref(false);
|
||||
return { active };
|
||||
},
|
||||
});
|
||||
defineProps<{
|
||||
showSidebarToggle?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
(e: 'toggle:sidebar'): void;
|
||||
}>();
|
||||
|
||||
const active = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -36,57 +36,46 @@
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import HeaderBarActions from './header-bar-actions.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { HeaderBarActions },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showSidebarToggle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
primaryActionIcon: {
|
||||
type: String,
|
||||
default: 'menu',
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
showSidebarToggle?: boolean;
|
||||
primaryActionIcon?: string;
|
||||
small?: boolean;
|
||||
shadow?: boolean;
|
||||
}>(),
|
||||
{
|
||||
primaryActionIcon: 'menu',
|
||||
shadow: true,
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: 'primary'): void;
|
||||
(e: 'toggle:sidebar'): void;
|
||||
}>();
|
||||
|
||||
const headerEl = ref<Element>();
|
||||
|
||||
const collapsed = ref(false);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => {
|
||||
collapsed.value = e.boundingClientRect.y === -1;
|
||||
},
|
||||
emits: ['primary', 'toggle:sidebar'],
|
||||
setup() {
|
||||
const headerEl = ref<Element>();
|
||||
{ threshold: [1] }
|
||||
);
|
||||
|
||||
const collapsed = ref(false);
|
||||
onMounted(() => {
|
||||
observer.observe(headerEl.value as HTMLElement);
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([e]) => {
|
||||
collapsed.value = e.boundingClientRect.y === -1;
|
||||
},
|
||||
{ threshold: [1] }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
observer.observe(headerEl.value as HTMLElement);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
return { headerEl, collapsed };
|
||||
},
|
||||
onUnmounted(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<v-list-item-icon><v-icon name="crop_square" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ t('square') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item clickable :active="aspectRatio === NaN" @click="aspectRatio = NaN">
|
||||
<v-list-item clickable :active="Number.isNaN(aspectRatio)" @click="aspectRatio = NaN">
|
||||
<v-list-item-icon><v-icon name="crop_free" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ t('free') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
@@ -123,17 +123,16 @@
|
||||
</v-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import api, { addTokenToURL } from '@/api';
|
||||
import { computed, defineComponent, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { getRootPath } from '@/utils/get-root-path';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import Cropper from 'cropperjs';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
type Image = {
|
||||
type: string;
|
||||
@@ -143,367 +142,337 @@ type Image = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
const props = defineProps<{
|
||||
id: string;
|
||||
modelValue?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
const { t, n } = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const localActive = ref(false);
|
||||
|
||||
const internalActive = computed({
|
||||
get() {
|
||||
return props.modelValue === undefined ? localActive.value : props.modelValue;
|
||||
},
|
||||
emits: ['update:modelValue', 'refresh'],
|
||||
setup(props, { emit }) {
|
||||
const { t, n } = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const localActive = ref(false);
|
||||
|
||||
const internalActive = computed({
|
||||
get() {
|
||||
return props.modelValue === undefined ? localActive.value : props.modelValue;
|
||||
},
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:modelValue', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
const { loading, error, imageData, imageElement, save, saving, fetchImage, onImageLoad } = useImage();
|
||||
|
||||
const {
|
||||
cropperInstance,
|
||||
initCropper,
|
||||
flip,
|
||||
rotate,
|
||||
reset,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
} = useCropper();
|
||||
|
||||
watch(internalActive, (isActive) => {
|
||||
if (isActive === true) {
|
||||
fetchImage();
|
||||
} else {
|
||||
if (cropperInstance.value) {
|
||||
cropperInstance.value.destroy();
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
imageData.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const randomId = ref<string>(nanoid());
|
||||
|
||||
const imageURL = computed(() => {
|
||||
return addTokenToURL(`${getRootPath()}assets/${props.id}?${randomId.value}`);
|
||||
});
|
||||
|
||||
const dimensionsString = computed(() => {
|
||||
let output = '';
|
||||
const isSVG = imageData.value?.type === 'image/svg+xml';
|
||||
|
||||
if (imageData.value) {
|
||||
if (isSVG) {
|
||||
output += 'SVG';
|
||||
} else {
|
||||
output += `${n(imageData.value.width ?? 0)}x${n(imageData.value.height ?? 0)}`;
|
||||
}
|
||||
|
||||
if (imageData.value.width !== newDimensions.width || imageData.value.height !== newDimensions.height) {
|
||||
if (isSVG) {
|
||||
if (newDimensions.width || newDimensions.height) {
|
||||
output += ` -> PNG ${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
|
||||
} else {
|
||||
output += ' -> PNG';
|
||||
}
|
||||
} else {
|
||||
output += ` -> ${isSVG ? 'PNG ' : ''}${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
const customAspectRatios = settingsStore.settings?.custom_aspect_ratios ?? null;
|
||||
|
||||
return {
|
||||
t,
|
||||
n,
|
||||
internalActive,
|
||||
loading,
|
||||
error,
|
||||
imageData,
|
||||
imageElement,
|
||||
save,
|
||||
onImageLoad,
|
||||
flip,
|
||||
rotate,
|
||||
reset,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
saving,
|
||||
imageURL,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
setAspectRatio,
|
||||
dimensionsString,
|
||||
customAspectRatios,
|
||||
};
|
||||
|
||||
function useImage() {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const imageData = ref<Image | null>(null);
|
||||
const saving = ref(false);
|
||||
|
||||
const imageElement = ref<HTMLImageElement | null>(null);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
imageData,
|
||||
saving,
|
||||
fetchImage,
|
||||
imageElement,
|
||||
save,
|
||||
onImageLoad,
|
||||
};
|
||||
|
||||
async function fetchImage() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const response = await api.get(`/files/${props.id}`, {
|
||||
params: {
|
||||
fields: ['type', 'filesize', 'filename_download', 'width', 'height'],
|
||||
},
|
||||
});
|
||||
|
||||
imageData.value = response.data.data;
|
||||
} catch (err: any) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
saving.value = true;
|
||||
|
||||
cropperInstance.value
|
||||
?.getCroppedCanvas({
|
||||
imageSmoothingQuality: 'high',
|
||||
})
|
||||
.toBlob(async (blob) => {
|
||||
if (blob === null) {
|
||||
saving.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, imageData.value?.filename_download);
|
||||
|
||||
try {
|
||||
await api.patch(`/files/${props.id}`, formData);
|
||||
emit('refresh');
|
||||
internalActive.value = false;
|
||||
randomId.value = nanoid();
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}, imageData.value?.type);
|
||||
}
|
||||
|
||||
async function onImageLoad() {
|
||||
await nextTick();
|
||||
initCropper();
|
||||
}
|
||||
}
|
||||
|
||||
function useCropper() {
|
||||
const cropperInstance = ref<Cropper | null>(null);
|
||||
|
||||
const localAspectRatio = ref(NaN);
|
||||
|
||||
const newDimensions = reactive({
|
||||
width: null as null | number,
|
||||
height: null as null | number,
|
||||
});
|
||||
|
||||
watch(imageData, () => {
|
||||
if (!imageData.value) return;
|
||||
localAspectRatio.value = imageData.value.width / imageData.value.height;
|
||||
newDimensions.width = imageData.value.width;
|
||||
newDimensions.height = imageData.value.height;
|
||||
});
|
||||
|
||||
const aspectRatio = computed<number>({
|
||||
get() {
|
||||
return localAspectRatio.value;
|
||||
},
|
||||
set(newAspectRatio) {
|
||||
localAspectRatio.value = newAspectRatio;
|
||||
cropperInstance.value?.setAspectRatio(newAspectRatio);
|
||||
cropperInstance.value?.crop();
|
||||
dragMode.value = 'crop';
|
||||
},
|
||||
});
|
||||
|
||||
const aspectRatioIcon = computed(() => {
|
||||
if (!imageData.value) return 'crop_original';
|
||||
|
||||
if (customAspectRatios) {
|
||||
const customAspectRatio = customAspectRatios.find((customAR) => customAR.value == aspectRatio.value);
|
||||
if (customAspectRatio) return 'crop_square';
|
||||
}
|
||||
|
||||
switch (aspectRatio.value) {
|
||||
case 16 / 9:
|
||||
return 'crop_16_9';
|
||||
case 3 / 2:
|
||||
return 'crop_3_2';
|
||||
case 5 / 4:
|
||||
return 'crop_5_4';
|
||||
case 7 / 5:
|
||||
return 'crop_7_5';
|
||||
case 1 / 1:
|
||||
return 'crop_square';
|
||||
case imageData.value.width / imageData.value.height:
|
||||
return 'crop_original';
|
||||
default:
|
||||
return 'crop_free';
|
||||
}
|
||||
});
|
||||
|
||||
const localDragMode = ref<'move' | 'crop'>('move');
|
||||
|
||||
const dragMode = computed({
|
||||
get() {
|
||||
return localDragMode.value;
|
||||
},
|
||||
set(newMode: 'move' | 'crop') {
|
||||
cropperInstance.value?.setDragMode(newMode);
|
||||
localDragMode.value = newMode;
|
||||
|
||||
if (newMode === 'move') {
|
||||
cropperInstance.value?.clear();
|
||||
localCropping.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const localCropping = ref(false);
|
||||
|
||||
const cropping = computed({
|
||||
get() {
|
||||
return localCropping.value;
|
||||
},
|
||||
set(newCropping: boolean) {
|
||||
if (newCropping === false) {
|
||||
cropperInstance.value?.clear();
|
||||
}
|
||||
|
||||
localCropping.value = newCropping;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
cropperInstance,
|
||||
initCropper,
|
||||
flip,
|
||||
rotate,
|
||||
reset,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
};
|
||||
|
||||
function initCropper() {
|
||||
if (imageElement.value === null) return;
|
||||
|
||||
if (cropperInstance.value) {
|
||||
cropperInstance.value.destroy();
|
||||
}
|
||||
|
||||
localCropping.value = false;
|
||||
|
||||
cropperInstance.value = new Cropper(imageElement.value, {
|
||||
autoCrop: false,
|
||||
autoCropArea: 0.5,
|
||||
toggleDragModeOnDblclick: false,
|
||||
dragMode: 'move',
|
||||
viewMode: 1,
|
||||
crop: throttle((event) => {
|
||||
if (!imageData.value) return;
|
||||
|
||||
if (cropping.value === false && (event.detail.width || event.detail.height)) {
|
||||
cropping.value = true;
|
||||
}
|
||||
|
||||
const newWidth = event.detail.width || imageData.value.width;
|
||||
const newHeight = event.detail.height || imageData.value.height;
|
||||
|
||||
if (event.detail.rotate === 0 || event.detail.rotate === -180) {
|
||||
newDimensions.width = Math.round(newWidth);
|
||||
newDimensions.height = Math.round(newHeight);
|
||||
} else {
|
||||
newDimensions.height = Math.round(newWidth);
|
||||
newDimensions.width = Math.round(newHeight);
|
||||
}
|
||||
}, 50),
|
||||
});
|
||||
}
|
||||
|
||||
function flip(type: 'horizontal' | 'vertical') {
|
||||
if (type === 'vertical') {
|
||||
if (cropperInstance.value?.getData().scaleX === -1) {
|
||||
cropperInstance.value?.scaleX(1);
|
||||
} else {
|
||||
cropperInstance.value?.scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'horizontal') {
|
||||
if (cropperInstance.value?.getData().scaleY === -1) {
|
||||
cropperInstance.value?.scaleY(1);
|
||||
} else {
|
||||
cropperInstance.value?.scaleY(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rotate() {
|
||||
cropperInstance.value?.rotate(-90);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
cropperInstance.value?.reset();
|
||||
dragMode.value = 'move';
|
||||
}
|
||||
}
|
||||
|
||||
function setAspectRatio() {
|
||||
if (imageData.value) {
|
||||
aspectRatio.value = imageData.value.width / imageData.value.height;
|
||||
}
|
||||
}
|
||||
set(newActive: boolean) {
|
||||
localActive.value = newActive;
|
||||
emit('update:modelValue', newActive);
|
||||
},
|
||||
});
|
||||
|
||||
const { loading, error, imageData, imageElement, save, saving, fetchImage, onImageLoad } = useImage();
|
||||
|
||||
const {
|
||||
cropperInstance,
|
||||
initCropper,
|
||||
flip,
|
||||
rotate,
|
||||
reset,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
} = useCropper();
|
||||
|
||||
watch(internalActive, (isActive) => {
|
||||
if (isActive === true) {
|
||||
fetchImage();
|
||||
} else {
|
||||
if (cropperInstance.value) {
|
||||
cropperInstance.value.destroy();
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
imageData.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const randomId = ref<string>(nanoid());
|
||||
|
||||
const imageURL = computed(() => {
|
||||
return addTokenToURL(`${getRootPath()}assets/${props.id}?${randomId.value}`);
|
||||
});
|
||||
|
||||
const dimensionsString = computed(() => {
|
||||
let output = '';
|
||||
const isSVG = imageData.value?.type === 'image/svg+xml';
|
||||
|
||||
if (imageData.value) {
|
||||
if (isSVG) {
|
||||
output += 'SVG';
|
||||
} else {
|
||||
output += `${n(imageData.value.width ?? 0)}x${n(imageData.value.height ?? 0)}`;
|
||||
}
|
||||
|
||||
if (imageData.value.width !== newDimensions.width || imageData.value.height !== newDimensions.height) {
|
||||
if (isSVG) {
|
||||
if (newDimensions.width || newDimensions.height) {
|
||||
output += ` -> PNG ${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
|
||||
} else {
|
||||
output += ' -> PNG';
|
||||
}
|
||||
} else {
|
||||
output += ` -> ${isSVG ? 'PNG ' : ''}${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
const customAspectRatios = settingsStore.settings?.custom_aspect_ratios ?? null;
|
||||
|
||||
function useImage() {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const imageData = ref<Image | null>(null);
|
||||
const saving = ref(false);
|
||||
|
||||
const imageElement = ref<HTMLImageElement | null>(null);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
imageData,
|
||||
saving,
|
||||
fetchImage,
|
||||
imageElement,
|
||||
save,
|
||||
onImageLoad,
|
||||
};
|
||||
|
||||
async function fetchImage() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const response = await api.get(`/files/${props.id}`, {
|
||||
params: {
|
||||
fields: ['type', 'filesize', 'filename_download', 'width', 'height'],
|
||||
},
|
||||
});
|
||||
|
||||
imageData.value = response.data.data;
|
||||
} catch (err: any) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
saving.value = true;
|
||||
|
||||
cropperInstance.value
|
||||
?.getCroppedCanvas({
|
||||
imageSmoothingQuality: 'high',
|
||||
})
|
||||
.toBlob(async (blob) => {
|
||||
if (blob === null) {
|
||||
saving.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, imageData.value?.filename_download);
|
||||
|
||||
try {
|
||||
await api.patch(`/files/${props.id}`, formData);
|
||||
emit('refresh');
|
||||
internalActive.value = false;
|
||||
randomId.value = nanoid();
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}, imageData.value?.type);
|
||||
}
|
||||
|
||||
async function onImageLoad() {
|
||||
await nextTick();
|
||||
initCropper();
|
||||
}
|
||||
}
|
||||
|
||||
function useCropper() {
|
||||
const cropperInstance = ref<Cropper | null>(null);
|
||||
|
||||
const localAspectRatio = ref(NaN);
|
||||
|
||||
const newDimensions = reactive({
|
||||
width: null as null | number,
|
||||
height: null as null | number,
|
||||
});
|
||||
|
||||
watch(imageData, () => {
|
||||
if (!imageData.value) return;
|
||||
localAspectRatio.value = imageData.value.width / imageData.value.height;
|
||||
newDimensions.width = imageData.value.width;
|
||||
newDimensions.height = imageData.value.height;
|
||||
});
|
||||
|
||||
const aspectRatio = computed<number>({
|
||||
get() {
|
||||
return localAspectRatio.value;
|
||||
},
|
||||
set(newAspectRatio) {
|
||||
localAspectRatio.value = newAspectRatio;
|
||||
cropperInstance.value?.setAspectRatio(newAspectRatio);
|
||||
cropperInstance.value?.crop();
|
||||
dragMode.value = 'crop';
|
||||
},
|
||||
});
|
||||
|
||||
const aspectRatioIcon = computed(() => {
|
||||
if (!imageData.value) return 'crop_original';
|
||||
|
||||
if (customAspectRatios) {
|
||||
const customAspectRatio = customAspectRatios.find((customAR) => customAR.value == aspectRatio.value);
|
||||
if (customAspectRatio) return 'crop_square';
|
||||
}
|
||||
|
||||
switch (aspectRatio.value) {
|
||||
case 16 / 9:
|
||||
return 'crop_16_9';
|
||||
case 3 / 2:
|
||||
return 'crop_3_2';
|
||||
case 5 / 4:
|
||||
return 'crop_5_4';
|
||||
case 7 / 5:
|
||||
return 'crop_7_5';
|
||||
case 1 / 1:
|
||||
return 'crop_square';
|
||||
case imageData.value.width / imageData.value.height:
|
||||
return 'crop_original';
|
||||
default:
|
||||
return 'crop_free';
|
||||
}
|
||||
});
|
||||
|
||||
const localDragMode = ref<'move' | 'crop'>('move');
|
||||
|
||||
const dragMode = computed({
|
||||
get() {
|
||||
return localDragMode.value;
|
||||
},
|
||||
set(newMode: 'move' | 'crop') {
|
||||
cropperInstance.value?.setDragMode(newMode);
|
||||
localDragMode.value = newMode;
|
||||
|
||||
if (newMode === 'move') {
|
||||
cropperInstance.value?.clear();
|
||||
localCropping.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const localCropping = ref(false);
|
||||
|
||||
const cropping = computed({
|
||||
get() {
|
||||
return localCropping.value;
|
||||
},
|
||||
set(newCropping: boolean) {
|
||||
if (newCropping === false) {
|
||||
cropperInstance.value?.clear();
|
||||
}
|
||||
|
||||
localCropping.value = newCropping;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
cropperInstance,
|
||||
initCropper,
|
||||
flip,
|
||||
rotate,
|
||||
reset,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
};
|
||||
|
||||
function initCropper() {
|
||||
if (imageElement.value === null) return;
|
||||
|
||||
if (cropperInstance.value) {
|
||||
cropperInstance.value.destroy();
|
||||
}
|
||||
|
||||
localCropping.value = false;
|
||||
|
||||
cropperInstance.value = new Cropper(imageElement.value, {
|
||||
autoCrop: false,
|
||||
autoCropArea: 0.5,
|
||||
toggleDragModeOnDblclick: false,
|
||||
dragMode: 'move',
|
||||
viewMode: 1,
|
||||
crop: throttle((event) => {
|
||||
if (!imageData.value) return;
|
||||
|
||||
if (cropping.value === false && (event.detail.width || event.detail.height)) {
|
||||
cropping.value = true;
|
||||
}
|
||||
|
||||
const newWidth = event.detail.width || imageData.value.width;
|
||||
const newHeight = event.detail.height || imageData.value.height;
|
||||
|
||||
if (event.detail.rotate === 0 || event.detail.rotate === -180) {
|
||||
newDimensions.width = Math.round(newWidth);
|
||||
newDimensions.height = Math.round(newHeight);
|
||||
} else {
|
||||
newDimensions.height = Math.round(newWidth);
|
||||
newDimensions.width = Math.round(newHeight);
|
||||
}
|
||||
}, 50),
|
||||
});
|
||||
}
|
||||
|
||||
function flip(type: 'horizontal' | 'vertical') {
|
||||
if (type === 'vertical') {
|
||||
if (cropperInstance.value?.getData().scaleX === -1) {
|
||||
cropperInstance.value?.scaleX(1);
|
||||
} else {
|
||||
cropperInstance.value?.scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'horizontal') {
|
||||
if (cropperInstance.value?.getData().scaleY === -1) {
|
||||
cropperInstance.value?.scaleY(1);
|
||||
} else {
|
||||
cropperInstance.value?.scaleY(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rotate() {
|
||||
cropperInstance.value?.rotate(-90);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
cropperInstance.value?.reset();
|
||||
dragMode.value = 'move';
|
||||
}
|
||||
}
|
||||
|
||||
function setAspectRatio() {
|
||||
if (imageData.value) {
|
||||
aspectRatio.value = imageData.value.width / imageData.value.height;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -5,76 +5,70 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useLatencyStore } from '@/stores/latency';
|
||||
import { sortBy } from 'lodash';
|
||||
import prettyMS from 'pretty-ms';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n();
|
||||
|
||||
const latencyStore = useLatencyStore();
|
||||
const latencyStore = useLatencyStore();
|
||||
|
||||
const lastLatency = computed(() => {
|
||||
const sorted = sortBy(latencyStore.latency, ['timestamp']);
|
||||
return sorted[sorted.length - 1];
|
||||
});
|
||||
const lastLatency = computed(() => {
|
||||
const sorted = sortBy(latencyStore.latency, ['timestamp']);
|
||||
return sorted[sorted.length - 1];
|
||||
});
|
||||
|
||||
const avgLatency = computed(() => {
|
||||
if (!latencyStore.latency || latencyStore.latency.length === 0) return 0;
|
||||
const sorted = sortBy(latencyStore.latency, ['timestamp']);
|
||||
const lastFive = sorted.slice(Math.max(sorted.length - 5, 0));
|
||||
let total = 0;
|
||||
const avgLatency = computed(() => {
|
||||
if (!latencyStore.latency || latencyStore.latency.length === 0) return 0;
|
||||
const sorted = sortBy(latencyStore.latency, ['timestamp']);
|
||||
const lastFive = sorted.slice(Math.max(sorted.length - 5, 0));
|
||||
let total = 0;
|
||||
|
||||
for (const { latency } of lastFive) {
|
||||
total += latency;
|
||||
}
|
||||
for (const { latency } of lastFive) {
|
||||
total += latency;
|
||||
}
|
||||
|
||||
return Math.round(total / lastFive.length);
|
||||
});
|
||||
return Math.round(total / lastFive.length);
|
||||
});
|
||||
|
||||
const connectionStrength = computed(() => {
|
||||
if (avgLatency.value <= 250) return 4;
|
||||
else if (avgLatency.value > 250 && avgLatency.value <= 500) return 3;
|
||||
else if (avgLatency.value > 500 && avgLatency.value <= 750) return 2;
|
||||
return 1;
|
||||
});
|
||||
const connectionStrength = computed(() => {
|
||||
if (avgLatency.value <= 250) return 4;
|
||||
else if (avgLatency.value > 250 && avgLatency.value <= 500) return 3;
|
||||
else if (avgLatency.value > 500 && avgLatency.value <= 750) return 2;
|
||||
return 1;
|
||||
});
|
||||
|
||||
const latencyTooltip = computed(() => {
|
||||
switch (connectionStrength.value) {
|
||||
case 4:
|
||||
return `${t('connection_excellent')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
case 3:
|
||||
return `${t('connection_good')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
case 2:
|
||||
return `${t('connection_fair')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
case 1:
|
||||
return `${t('connection_poor')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const latencyTooltip = computed(() => {
|
||||
switch (connectionStrength.value) {
|
||||
case 4:
|
||||
return `${t('connection_excellent')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
case 3:
|
||||
return `${t('connection_good')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
case 2:
|
||||
return `${t('connection_fair')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
case 1:
|
||||
return `${t('connection_poor')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (connectionStrength.value) {
|
||||
case 4:
|
||||
return 'signal_wifi_4_bar';
|
||||
case 3:
|
||||
return 'signal_wifi_3_bar';
|
||||
case 2:
|
||||
return 'signal_wifi_2_bar';
|
||||
case 1:
|
||||
return 'signal_wifi_1_bar';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return { icon, lastLatency, latencyTooltip };
|
||||
},
|
||||
const icon = computed(() => {
|
||||
switch (connectionStrength.value) {
|
||||
case 4:
|
||||
return 'signal_wifi_4_bar';
|
||||
case 3:
|
||||
return 'signal_wifi_3_bar';
|
||||
case 2:
|
||||
return 'signal_wifi_2_bar';
|
||||
case 1:
|
||||
return 'signal_wifi_1_bar';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,35 +15,35 @@
|
||||
</sidebar-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useSync } from '@directus/composables';
|
||||
import { useExtensions } from '@/extensions';
|
||||
import { useExtension } from '@/composables/use-extension';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: 'tabular',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string;
|
||||
}>(),
|
||||
{
|
||||
modelValue: 'tabular',
|
||||
}
|
||||
);
|
||||
|
||||
const { layouts } = useExtensions();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const selectedLayout = useExtension('layout', props.modelValue);
|
||||
const fallbackLayout = useExtension('layout', 'tabular');
|
||||
const currentLayout = computed(() => selectedLayout.value ?? fallbackLayout.value);
|
||||
const { t } = useI18n();
|
||||
|
||||
const layout = useSync(props, 'modelValue', emit);
|
||||
const { layouts } = useExtensions();
|
||||
|
||||
return { t, currentLayout, layouts, layout };
|
||||
},
|
||||
});
|
||||
const selectedLayout = useExtension('layout', props.modelValue);
|
||||
const fallbackLayout = useExtension('layout', 'tabular');
|
||||
const currentLayout = computed(() => selectedLayout.value ?? fallbackLayout.value);
|
||||
|
||||
const layout = useSync(props, 'modelValue', emit);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -8,22 +8,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import LatencyIndicator from './latency-indicator.vue';
|
||||
import { useServerStore } from '@/stores/server';
|
||||
|
||||
export default defineComponent({
|
||||
components: { LatencyIndicator },
|
||||
setup() {
|
||||
const serverStore = useServerStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const name = computed(() => serverStore.info?.project?.project_name);
|
||||
const descriptor = computed(() => serverStore.info?.project?.project_descriptor);
|
||||
|
||||
return { name, descriptor };
|
||||
},
|
||||
});
|
||||
const name = computed(() => serverStore.info?.project?.project_name);
|
||||
const descriptor = computed(() => serverStore.info?.project?.project_descriptor);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -9,76 +9,77 @@
|
||||
</sidebar-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, defineComponent, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number | null;
|
||||
}>(),
|
||||
{
|
||||
modelValue: null,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number | null): void;
|
||||
(e: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const interval = computed<number | null>({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
emits: ['update:modelValue', 'refresh'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const interval = computed<number | null>({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(newVal) {
|
||||
emit('update:modelValue', newVal);
|
||||
},
|
||||
});
|
||||
|
||||
const activeInterval = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
watch(
|
||||
interval,
|
||||
(newInterval) => {
|
||||
if (activeInterval.value !== null) {
|
||||
clearInterval(activeInterval.value);
|
||||
}
|
||||
|
||||
if (newInterval !== null && newInterval > 0) {
|
||||
activeInterval.value = setInterval(() => {
|
||||
emit('refresh');
|
||||
}, newInterval * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const items = computed(() => {
|
||||
const intervals = [null, 10, 30, 60, 300];
|
||||
|
||||
return intervals.map((seconds) => {
|
||||
if (seconds === null) {
|
||||
return {
|
||||
text: t('no_refresh'),
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
|
||||
return seconds >= 60 && seconds % 60 === 0
|
||||
? {
|
||||
text: t('refresh_interval_minutes', { minutes: seconds / 60 }, seconds / 60),
|
||||
value: seconds,
|
||||
}
|
||||
: {
|
||||
text: t('refresh_interval_seconds', { seconds }, seconds),
|
||||
value: seconds,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const active = computed(() => interval.value !== null);
|
||||
|
||||
return { t, active, interval, items };
|
||||
set(newVal) {
|
||||
emit('update:modelValue', newVal);
|
||||
},
|
||||
});
|
||||
|
||||
const activeInterval = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
watch(
|
||||
interval,
|
||||
(newInterval) => {
|
||||
if (activeInterval.value !== null) {
|
||||
clearInterval(activeInterval.value);
|
||||
}
|
||||
|
||||
if (newInterval !== null && newInterval > 0) {
|
||||
activeInterval.value = setInterval(() => {
|
||||
emit('refresh');
|
||||
}, newInterval * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const items = computed(() => {
|
||||
const intervals = [null, 10, 30, 60, 300];
|
||||
|
||||
return intervals.map((seconds) => {
|
||||
if (seconds === null) {
|
||||
return {
|
||||
text: t('no_refresh'),
|
||||
value: null,
|
||||
};
|
||||
}
|
||||
|
||||
return seconds >= 60 && seconds % 60 === 0
|
||||
? {
|
||||
text: t('refresh_interval_minutes', { minutes: seconds / 60 }, seconds / 60),
|
||||
value: seconds,
|
||||
}
|
||||
: {
|
||||
text: t('refresh_interval_seconds', { seconds }, seconds),
|
||||
value: seconds,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const active = computed(() => interval.value !== null);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -19,51 +19,24 @@
|
||||
</v-error-boundary>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useExtension } from '@/composables/use-extension';
|
||||
import { defineComponent } from 'vue';
|
||||
import { toRefs } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
display: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
interface: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
interfaceOptions: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Object, Array, Boolean],
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const displayInfo = useExtension('display', props.display);
|
||||
const props = defineProps<{
|
||||
display: string | null;
|
||||
options?: Record<string, unknown>;
|
||||
interface?: string;
|
||||
interfaceOptions?: Record<string, unknown>;
|
||||
value?: string | number | boolean | Record<string, unknown> | unknown[];
|
||||
type: string;
|
||||
collection: string;
|
||||
field: string;
|
||||
}>();
|
||||
|
||||
return { displayInfo };
|
||||
},
|
||||
});
|
||||
const { display } = toRefs(props);
|
||||
|
||||
const displayInfo = useExtension('display', display);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -53,7 +53,7 @@ import RevisionsDrawerUpdates from './revisions-drawer-updates.vue';
|
||||
const props = defineProps<{
|
||||
revisions: Revision[];
|
||||
current?: number | string | null;
|
||||
active?: boolean;
|
||||
active: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -5,21 +5,21 @@
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-if="!disabledOptions.includes('save-and-stay')" clickable @click="$emit('save-and-stay')">
|
||||
<v-list-item v-if="!disabledOptions?.includes('save-and-stay')" clickable @click="$emit('save-and-stay')">
|
||||
<v-list-item-icon><v-icon name="check" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ t('save_and_stay') }}</v-list-item-content>
|
||||
<v-list-item-hint>{{ translateShortcut(['meta', 's']) }}</v-list-item-hint>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="!disabledOptions.includes('save-and-add-new')" clickable @click="$emit('save-and-add-new')">
|
||||
<v-list-item v-if="!disabledOptions?.includes('save-and-add-new')" clickable @click="$emit('save-and-add-new')">
|
||||
<v-list-item-icon><v-icon name="add" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ t('save_and_create_new') }}</v-list-item-content>
|
||||
<v-list-item-hint>{{ translateShortcut(['meta', 'shift', 's']) }}</v-list-item-hint>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="!disabledOptions.includes('save-as-copy')" clickable @click="$emit('save-as-copy')">
|
||||
<v-list-item v-if="!disabledOptions?.includes('save-as-copy')" clickable @click="$emit('save-as-copy')">
|
||||
<v-list-item-icon><v-icon name="done_all" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ t('save_as_copy') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="!disabledOptions.includes('discard-and-stay')" clickable @click="$emit('discard-and-stay')">
|
||||
<v-list-item v-if="!disabledOptions?.includes('discard-and-stay')" clickable @click="$emit('discard-and-stay')">
|
||||
<v-list-item-icon><v-icon name="undo" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ t('discard_all_changes') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
@@ -27,25 +27,22 @@
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent } from 'vue';
|
||||
import { translateShortcut } from '@/utils/translate-shortcut';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
disabledOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['save-and-stay', 'save-and-add-new', 'save-as-copy', 'discard-and-stay'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
defineProps<{
|
||||
disabledOptions?: string[];
|
||||
}>();
|
||||
|
||||
return { t, translateShortcut };
|
||||
},
|
||||
});
|
||||
defineEmits<{
|
||||
(e: 'save-and-stay'): void;
|
||||
(e: 'save-and-add-new'): void;
|
||||
(e: 'save-as-copy'): void;
|
||||
(e: 'discard-and-stay'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -42,134 +42,113 @@
|
||||
</v-badge>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, watch, PropType, computed, inject, Ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { useElementSize } from '@directus/composables';
|
||||
import { Filter } from '@directus/types';
|
||||
import { isObject } from 'lodash';
|
||||
import { useElementSize } from '@directus/composables';
|
||||
import { Ref, computed, inject, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
filter: {
|
||||
type: Object as PropType<Filter>,
|
||||
default: null,
|
||||
},
|
||||
const props = defineProps<{
|
||||
modelValue: string | null;
|
||||
collection: string;
|
||||
filter?: Filter | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | null): void;
|
||||
(e: 'update:filter', value: Filter | null): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const active = ref(props.modelValue !== null);
|
||||
const filterActive = ref(false);
|
||||
const filterBorder = ref(false);
|
||||
|
||||
const mainElement = inject<Ref<Element | undefined>>('main-element');
|
||||
const filterElement = ref<HTMLElement>();
|
||||
const { width: mainElementWidth } = useElementSize(mainElement!);
|
||||
const { width: filterElementWidth } = useElementSize(filterElement);
|
||||
|
||||
watch(
|
||||
[mainElementWidth, filterElementWidth],
|
||||
() => {
|
||||
if (!filterElement.value) return;
|
||||
|
||||
const searchElement = filterElement.value.parentElement!;
|
||||
const minWidth = searchElement.offsetWidth - 4;
|
||||
|
||||
if (filterElementWidth.value > minWidth) {
|
||||
filterElement.value.style.borderTopLeftRadius =
|
||||
filterElementWidth.value > minWidth + 22 ? 22 + 'px' : filterElementWidth.value - minWidth + 'px';
|
||||
} else {
|
||||
filterElement.value.style.borderTopLeftRadius = '0px';
|
||||
}
|
||||
|
||||
const headerElement = mainElement?.value?.firstElementChild;
|
||||
|
||||
if (!headerElement) return;
|
||||
|
||||
const maxWidth =
|
||||
searchElement.getBoundingClientRect().right -
|
||||
(headerElement.getBoundingClientRect().left +
|
||||
Number(window.getComputedStyle(headerElement).paddingLeft.replace('px', '')));
|
||||
|
||||
filterElement.value.style.maxWidth = maxWidth > minWidth ? `${String(maxWidth)}px` : '0px';
|
||||
},
|
||||
emits: ['update:modelValue', 'update:filter'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const active = ref(props.modelValue !== null);
|
||||
const filterActive = ref(false);
|
||||
const filterBorder = ref(false);
|
||||
|
||||
const mainElement = inject<Ref<Element | undefined>>('main-element');
|
||||
const filterElement = ref<HTMLElement>();
|
||||
const { width: mainElementWidth } = useElementSize(mainElement!);
|
||||
const { width: filterElementWidth } = useElementSize(filterElement);
|
||||
|
||||
watch(
|
||||
[mainElementWidth, filterElementWidth],
|
||||
() => {
|
||||
if (!filterElement.value) return;
|
||||
|
||||
const searchElement = filterElement.value.parentElement!;
|
||||
const minWidth = searchElement.offsetWidth - 4;
|
||||
|
||||
if (filterElementWidth.value > minWidth) {
|
||||
filterElement.value.style.borderTopLeftRadius =
|
||||
filterElementWidth.value > minWidth + 22 ? 22 + 'px' : filterElementWidth.value - minWidth + 'px';
|
||||
} else {
|
||||
filterElement.value.style.borderTopLeftRadius = '0px';
|
||||
}
|
||||
|
||||
const headerElement = mainElement?.value?.firstElementChild;
|
||||
|
||||
if (!headerElement) return;
|
||||
|
||||
const maxWidth =
|
||||
searchElement.getBoundingClientRect().right -
|
||||
(headerElement.getBoundingClientRect().left +
|
||||
Number(window.getComputedStyle(headerElement).paddingLeft.replace('px', '')));
|
||||
|
||||
filterElement.value.style.maxWidth = maxWidth > minWidth ? `${String(maxWidth)}px` : '0px';
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(active, (newActive: boolean) => {
|
||||
if (newActive === true && input.value !== null) {
|
||||
input.value.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const activeFilterCount = computed(() => {
|
||||
if (!props.filter) return 0;
|
||||
|
||||
let filterOperators: string[] = [];
|
||||
|
||||
parseLevel(props.filter);
|
||||
|
||||
return filterOperators.length;
|
||||
|
||||
function parseLevel(level: Record<string, any>) {
|
||||
for (const [key, value] of Object.entries(level)) {
|
||||
if (key === '_and' || key === '_or') {
|
||||
value.forEach(parseLevel);
|
||||
} else if (key.startsWith('_')) {
|
||||
filterOperators.push(key);
|
||||
} else {
|
||||
if (isObject(value)) {
|
||||
parseLevel(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
t,
|
||||
active,
|
||||
disable,
|
||||
input,
|
||||
emitValue,
|
||||
activeFilterCount,
|
||||
filterActive,
|
||||
onClickOutside,
|
||||
filterBorder,
|
||||
filterElement,
|
||||
};
|
||||
|
||||
function onClickOutside(e: { path?: HTMLElement[]; composedPath?: () => HTMLElement[] }) {
|
||||
const path = e.path || e.composedPath!();
|
||||
if (path.some((el) => el?.classList?.contains('v-menu-content'))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function disable() {
|
||||
active.value = false;
|
||||
filterActive.value = false;
|
||||
}
|
||||
|
||||
function emitValue() {
|
||||
if (!input.value) return;
|
||||
const value = input.value?.value;
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
},
|
||||
watch(active, (newActive: boolean) => {
|
||||
if (newActive === true && input.value !== null) {
|
||||
input.value.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const activeFilterCount = computed(() => {
|
||||
if (!props.filter) return 0;
|
||||
|
||||
let filterOperators: string[] = [];
|
||||
|
||||
parseLevel(props.filter);
|
||||
|
||||
return filterOperators.length;
|
||||
|
||||
function parseLevel(level: Record<string, any>) {
|
||||
for (const [key, value] of Object.entries(level)) {
|
||||
if (key === '_and' || key === '_or') {
|
||||
value.forEach(parseLevel);
|
||||
} else if (key.startsWith('_')) {
|
||||
filterOperators.push(key);
|
||||
} else {
|
||||
if (isObject(value)) {
|
||||
parseLevel(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function onClickOutside(e: { path?: HTMLElement[]; composedPath?: () => HTMLElement[] }) {
|
||||
const path = e.path || e.composedPath!();
|
||||
if (path.some((el) => el?.classList?.contains('v-menu-content'))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function disable() {
|
||||
active.value = false;
|
||||
filterActive.value = false;
|
||||
}
|
||||
|
||||
function emitValue() {
|
||||
if (!input.value) return;
|
||||
const value = input.value?.value;
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -49,56 +49,53 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { format } from 'date-fns';
|
||||
<script setup lang="ts">
|
||||
import { isAllowed } from '@/utils/is-allowed';
|
||||
import { Share } from '@directus/types';
|
||||
import { format } from 'date-fns';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
share: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['copy', 'edit', 'invite', 'delete'],
|
||||
setup(props) {
|
||||
const { t, d } = useI18n();
|
||||
const props = defineProps<{
|
||||
share: Share;
|
||||
}>();
|
||||
|
||||
const editAllowed = computed(() => {
|
||||
return isAllowed('directus_shares', 'update', props.share);
|
||||
});
|
||||
defineEmits<{
|
||||
(e: 'copy'): void;
|
||||
(e: 'edit'): void;
|
||||
(e: 'invite'): void;
|
||||
(e: 'delete'): void;
|
||||
}>();
|
||||
|
||||
const deleteAllowed = computed(() => {
|
||||
return isAllowed('directus_shares', 'delete', props.share);
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
const usesLeft = computed(() => {
|
||||
if (props.share.max_uses === null) return null;
|
||||
return props.share.max_uses - props.share.times_used;
|
||||
});
|
||||
const editAllowed = computed(() => {
|
||||
return isAllowed('directus_shares', 'update', props.share);
|
||||
});
|
||||
|
||||
const status = computed(() => {
|
||||
if (props.share.date_end && new Date(props.share.date_end) < new Date()) {
|
||||
return 'expired';
|
||||
}
|
||||
const deleteAllowed = computed(() => {
|
||||
return isAllowed('directus_shares', 'delete', props.share);
|
||||
});
|
||||
|
||||
if (props.share.date_start && new Date(props.share.date_start) > new Date()) {
|
||||
return 'upcoming';
|
||||
}
|
||||
const usesLeft = computed(() => {
|
||||
if (props.share.max_uses === null) return null;
|
||||
return props.share.max_uses - props.share.times_used;
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
const status = computed(() => {
|
||||
if (props.share.date_end && new Date(props.share.date_end) < new Date()) {
|
||||
return 'expired';
|
||||
}
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
return format(new Date(props.share.date_created), String(t('date-fns_date_short')));
|
||||
});
|
||||
if (props.share.date_start && new Date(props.share.date_start) > new Date()) {
|
||||
return 'upcoming';
|
||||
}
|
||||
|
||||
const confirmDelete = ref<string | null>(null);
|
||||
return null;
|
||||
});
|
||||
|
||||
return { editAllowed, deleteAllowed, usesLeft, status, t, d, formattedTime, confirmDelete };
|
||||
},
|
||||
const formattedTime = computed(() => {
|
||||
return format(new Date(props.share.date_created), String(t('date-fns_date_short')));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -74,10 +74,9 @@
|
||||
</sidebar-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, computed } from 'vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { getRootPath } from '@/utils/get-root-path';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { Share } from '@directus/types';
|
||||
@@ -85,179 +84,145 @@ import { useClipboard } from '@/composables/use-clipboard';
|
||||
|
||||
import api from '@/api';
|
||||
import ShareItem from './share-item.vue';
|
||||
import DrawerItem from '@/views/private/components/drawer-item.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ShareItem, DrawerItem },
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
primaryKey: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
allowed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
collection: string;
|
||||
primaryKey: string | number;
|
||||
allowed: boolean;
|
||||
}>();
|
||||
|
||||
const { copyToClipboard } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
|
||||
const shares = ref<Share[] | null>(null);
|
||||
const count = ref(0);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
const deleting = ref(false);
|
||||
const shareToEdit = ref<string | null>(null);
|
||||
const shareToSend = ref<Share | null>(null);
|
||||
const shareToDelete = ref<Share | null>(null);
|
||||
const sending = ref(false);
|
||||
const sendEmails = ref('');
|
||||
const { copyToClipboard } = useClipboard();
|
||||
|
||||
const sendPublicLink = computed(() => {
|
||||
if (!shareToSend.value) return null;
|
||||
return window.location.origin + getRootPath() + 'admin/shared/' + shareToSend.value.id;
|
||||
const shares = ref<Share[]>([]);
|
||||
const count = ref(0);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
const deleting = ref(false);
|
||||
const shareToEdit = ref<string | null>(null);
|
||||
const shareToSend = ref<Share | null>(null);
|
||||
const shareToDelete = ref<Share | null>(null);
|
||||
const sending = ref(false);
|
||||
const sendEmails = ref('');
|
||||
|
||||
const sendPublicLink = computed(() => {
|
||||
if (!shareToSend.value) return null;
|
||||
return window.location.origin + getRootPath() + 'admin/shared/' + shareToSend.value.id;
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
async function input(data: any) {
|
||||
if (!data) return;
|
||||
|
||||
data.collection = props.collection;
|
||||
data.item = props.primaryKey;
|
||||
|
||||
try {
|
||||
if (shareToEdit.value === '+') {
|
||||
await api.post('/shares', data);
|
||||
} else {
|
||||
await api.patch(`/shares/${shareToEdit.value}`, data);
|
||||
}
|
||||
|
||||
await refresh();
|
||||
|
||||
shareToEdit.value = null;
|
||||
} catch (error: any) {
|
||||
unexpectedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function copy(id: string) {
|
||||
const url = window.location.origin + getRootPath() + 'admin/shared/' + id;
|
||||
await copyToClipboard(url, { success: t('share_copy_link_success'), fail: t('share_copy_link_error') });
|
||||
}
|
||||
|
||||
function select(id: string) {
|
||||
shareToEdit.value = id;
|
||||
}
|
||||
|
||||
function unselect() {
|
||||
shareToEdit.value = null;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
error.value = null;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/shares`, {
|
||||
params: {
|
||||
filter: {
|
||||
_and: [
|
||||
{
|
||||
collection: {
|
||||
_eq: props.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
item: {
|
||||
_eq: props.primaryKey,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
sort: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
refresh();
|
||||
count.value = response.data.data.length;
|
||||
shares.value = response.data.data;
|
||||
} catch (error: any) {
|
||||
error.value = error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shareToDelete,
|
||||
t,
|
||||
shares,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
count,
|
||||
select,
|
||||
unselect,
|
||||
shareToEdit,
|
||||
input,
|
||||
copy,
|
||||
shareToSend,
|
||||
remove,
|
||||
deleting,
|
||||
sendPublicLink,
|
||||
send,
|
||||
sending,
|
||||
sendEmails,
|
||||
};
|
||||
async function remove() {
|
||||
if (!shareToDelete.value) return;
|
||||
|
||||
async function input(data: any) {
|
||||
if (!data) return;
|
||||
deleting.value = true;
|
||||
|
||||
data.collection = props.collection;
|
||||
data.item = props.primaryKey;
|
||||
try {
|
||||
await api.delete(`/shares/${shareToDelete.value.id}`);
|
||||
await refresh();
|
||||
shareToDelete.value = null;
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (shareToEdit.value === '+') {
|
||||
await api.post('/shares', data);
|
||||
} else {
|
||||
await api.patch(`/shares/${shareToEdit.value}`, data);
|
||||
}
|
||||
async function send() {
|
||||
if (!shareToSend.value) return;
|
||||
|
||||
await refresh();
|
||||
sending.value = true;
|
||||
|
||||
shareToEdit.value = null;
|
||||
} catch (error: any) {
|
||||
unexpectedError(error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const emailsParsed = sendEmails.value
|
||||
.split(/,|\n/)
|
||||
.filter((e) => e)
|
||||
.map((email) => email.trim());
|
||||
|
||||
async function copy(id: string) {
|
||||
const url = window.location.origin + getRootPath() + 'admin/shared/' + id;
|
||||
await copyToClipboard(url, { success: t('share_copy_link_success'), fail: t('share_copy_link_error') });
|
||||
}
|
||||
await api.post('/shares/invite', {
|
||||
emails: emailsParsed,
|
||||
share: shareToSend.value.id,
|
||||
});
|
||||
|
||||
function select(id: string) {
|
||||
shareToEdit.value = id;
|
||||
}
|
||||
sendEmails.value = '';
|
||||
|
||||
function unselect() {
|
||||
shareToEdit.value = null;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
error.value = null;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/shares`, {
|
||||
params: {
|
||||
filter: {
|
||||
_and: [
|
||||
{
|
||||
collection: {
|
||||
_eq: props.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
item: {
|
||||
_eq: props.primaryKey,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
sort: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
count.value = response.data.data.length;
|
||||
shares.value = response.data.data;
|
||||
} catch (error: any) {
|
||||
error.value = error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
if (!shareToDelete.value) return;
|
||||
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
await api.delete(`/shares/${shareToDelete.value.id}`);
|
||||
await refresh();
|
||||
shareToDelete.value = null;
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (!shareToSend.value) return;
|
||||
|
||||
sending.value = true;
|
||||
|
||||
try {
|
||||
const emailsParsed = sendEmails.value
|
||||
.split(/,|\n/)
|
||||
.filter((e) => e)
|
||||
.map((email) => email.trim());
|
||||
|
||||
await api.post('/shares/invite', {
|
||||
emails: emailsParsed,
|
||||
share: shareToSend.value.id,
|
||||
});
|
||||
|
||||
sendEmails.value = '';
|
||||
|
||||
shareToSend.value = null;
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
sending.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
shareToSend.value = null;
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
sending.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<div class="icon">
|
||||
<v-icon :name="icon" />
|
||||
<v-icon :name="icon!" />
|
||||
</div>
|
||||
<div v-if="sidebarOpen" class="title">
|
||||
<slot />
|
||||
@@ -15,8 +15,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { toRefs } from 'vue';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
||||
@@ -22,87 +22,79 @@
|
||||
</v-avatar>
|
||||
<div class="data">
|
||||
<div class="name type-title">{{ userName(data) }}</div>
|
||||
<div class="status-role" :class="data.status">{{ t(data.status) }} {{ data.role.name }}</div>
|
||||
<div class="status-role" :class="data!.status">{{ t(data.status) }} {{ data.role.name }}</div>
|
||||
<div class="email">{{ data.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import api from '@/api';
|
||||
import { userName } from '@/utils/user-name';
|
||||
import { User } from '@directus/types';
|
||||
import { computed, defineComponent, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
user: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
user: string;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const data = ref<User | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const avatarSrc = computed(() => {
|
||||
if (data.value === null) return null;
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const data = ref<User | null>(null);
|
||||
|
||||
if (data.value.avatar?.id) {
|
||||
return `/assets/${data.value.avatar.id}?key=system-medium-cover`;
|
||||
}
|
||||
const avatarSrc = computed(() => {
|
||||
if (data.value === null) return null;
|
||||
|
||||
return null;
|
||||
});
|
||||
if (data.value.avatar?.id) {
|
||||
return `/assets/${data.value.avatar.id}?key=system-medium-cover`;
|
||||
}
|
||||
|
||||
const active = ref(false);
|
||||
|
||||
watch(active, () => {
|
||||
if (active.value === true && data.value === null && loading.value === false) {
|
||||
fetchUser();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
data.value = null;
|
||||
});
|
||||
|
||||
return { t, loading, error, data, active, avatarSrc, userName, navigateToUser };
|
||||
|
||||
async function fetchUser() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/users/${props.user}`, {
|
||||
params: {
|
||||
fields: ['id', 'first_name', 'last_name', 'avatar.id', 'role.name', 'status', 'email'],
|
||||
},
|
||||
});
|
||||
|
||||
data.value = response.data.data;
|
||||
} catch (err: any) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToUser() {
|
||||
if (data.value) router.push(`/users/${data.value.id}`);
|
||||
}
|
||||
},
|
||||
return null;
|
||||
});
|
||||
|
||||
const active = ref(false);
|
||||
|
||||
watch(active, () => {
|
||||
if (active.value === true && data.value === null && loading.value === false) {
|
||||
fetchUser();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
data.value = null;
|
||||
});
|
||||
|
||||
async function fetchUser() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await api.get(`/users/${props.user}`, {
|
||||
params: {
|
||||
fields: ['id', 'first_name', 'last_name', 'avatar.id', 'role.name', 'status', 'email'],
|
||||
},
|
||||
});
|
||||
|
||||
data.value = response.data.data;
|
||||
} catch (err: any) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToUser() {
|
||||
if (data.value) router.push(`/users/${data.value.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -40,96 +40,93 @@
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
import api from '@/api';
|
||||
import { ref, watch } from 'vue';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import { APIError } from '@/types/error';
|
||||
import api from '@/api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
role?: string | null;
|
||||
}>(),
|
||||
{
|
||||
role: null,
|
||||
}
|
||||
);
|
||||
|
||||
const emails = ref<string>('');
|
||||
const roles = ref<Record<string, any>[]>([]);
|
||||
const roleSelected = ref<string | null>(props.role);
|
||||
const loading = ref(false);
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const uniqueValidationErrors = ref([]);
|
||||
const { t } = useI18n();
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
loadRoles();
|
||||
}
|
||||
const emails = ref<string>('');
|
||||
const roles = ref<Record<string, any>[]>([]);
|
||||
const roleSelected = ref<string | null>(props.role);
|
||||
const loading = ref(false);
|
||||
|
||||
const uniqueValidationErrors = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
loadRoles();
|
||||
}
|
||||
);
|
||||
|
||||
async function inviteUsers() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const emailsParsed = emails.value
|
||||
.split(/,|\n/)
|
||||
.filter((e) => e)
|
||||
.map((email) => email.trim());
|
||||
|
||||
await api.post('/users/invite', {
|
||||
email: emailsParsed,
|
||||
role: roleSelected.value,
|
||||
});
|
||||
|
||||
emails.value = '';
|
||||
emit('update:modelValue', false);
|
||||
} catch (err: any) {
|
||||
uniqueValidationErrors.value = err?.response?.data?.errors?.filter((error: APIError) => {
|
||||
return error.extensions?.code === 'RECORD_NOT_UNIQUE';
|
||||
});
|
||||
|
||||
const otherErrors = err?.response?.data?.errors?.filter(
|
||||
(err: APIError) => err?.extensions?.code !== 'RECORD_NOT_UNIQUE'
|
||||
);
|
||||
|
||||
return { t, emails, inviteUsers, roles, roleSelected, loading, uniqueValidationErrors };
|
||||
|
||||
async function inviteUsers() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const emailsParsed = emails.value
|
||||
.split(/,|\n/)
|
||||
.filter((e) => e)
|
||||
.map((email) => email.trim());
|
||||
|
||||
await api.post('/users/invite', {
|
||||
email: emailsParsed,
|
||||
role: roleSelected.value,
|
||||
});
|
||||
|
||||
emails.value = '';
|
||||
emit('update:modelValue', false);
|
||||
} catch (err: any) {
|
||||
uniqueValidationErrors.value = err?.response?.data?.errors?.filter((error: APIError) => {
|
||||
return error.extensions?.code === 'RECORD_NOT_UNIQUE';
|
||||
});
|
||||
|
||||
const otherErrors = err?.response?.data?.errors?.filter(
|
||||
(err: APIError) => err?.extensions?.code !== 'RECORD_NOT_UNIQUE'
|
||||
);
|
||||
|
||||
if (otherErrors.length > 0) {
|
||||
otherErrors.forEach((err: APIError) => unexpectedError(err));
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
if (otherErrors.length > 0) {
|
||||
otherErrors.forEach((err: APIError) => unexpectedError(err));
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const response = await api.get('/roles', {
|
||||
params: {
|
||||
sort: 'name',
|
||||
fields: ['id', 'name'],
|
||||
},
|
||||
});
|
||||
async function loadRoles() {
|
||||
const response = await api.get('/roles', {
|
||||
params: {
|
||||
sort: 'name',
|
||||
fields: ['id', 'name'],
|
||||
},
|
||||
});
|
||||
|
||||
roles.value = response.data.data.map((role: Record<string, any>) => ({
|
||||
text: role.name,
|
||||
value: role.id,
|
||||
}));
|
||||
roles.value = response.data.data.map((role: Record<string, any>) => ({
|
||||
text: role.name,
|
||||
value: role.id,
|
||||
}));
|
||||
|
||||
if (roles.value.length > 0 && roleSelected.value === null) {
|
||||
roleSelected.value = roles.value[0].value;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
if (roles.value.length > 0 && roleSelected.value === null) {
|
||||
roleSelected.value = roles.value[0].value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user