diff --git a/package-lock.json b/package-lock.json
index f48650a52..6430b6e63 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,7 +40,6 @@
"vue": "^3.5.x",
"vue-i18n": "~11.x",
"vue-router": "^4.4.3",
- "vue2-dropzone": "^3.6.0",
"vuex": "^4.1.0",
"webitel-sdk": "^26.4.29",
"zod": "^4.x"
@@ -5611,12 +5610,6 @@
"url": "https://dotenvx.com"
}
},
- "node_modules/dropzone": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz",
- "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==",
- "license": "MIT"
- },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -11671,15 +11664,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/vue2-dropzone": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/vue2-dropzone/-/vue2-dropzone-3.6.0.tgz",
- "integrity": "sha512-YXC1nCWIZvfa98e/i6h+EshZCkFSxFEh0Sxr9ODfThAPPDVhAzLLlz/4XIx0NGO1QeSy6htwSstte47R7vVhLQ==",
- "license": "MIT",
- "dependencies": {
- "dropzone": "^5.5.1"
- }
- },
"node_modules/vuex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
diff --git a/package.json b/package.json
index 760f9e44a..d7f410800 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,6 @@
"vue": "^3.5.x",
"vue-i18n": "~11.x",
"vue-router": "^4.4.3",
- "vue2-dropzone": "^3.6.0",
"vuex": "^4.1.0",
"webitel-sdk": "^26.4.29",
"zod": "^4.x"
diff --git a/public/animations/equalizer/eq.html b/public/animations/equalizer/eq.html
deleted file mode 100644
index 79f97e221..000000000
--- a/public/animations/equalizer/eq.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
-
-eq
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/animations/equalizer/eq.js b/public/animations/equalizer/eq.js
deleted file mode 100644
index 49d691c85..000000000
--- a/public/animations/equalizer/eq.js
+++ /dev/null
@@ -1,358 +0,0 @@
-(function (cjs, an) {
-
-var p; // shortcut to reference prototypes
-var lib={};var ss={};var img={};
-lib.ssMetadata = [
- {name:"eq_atlas_", frames: [[11,33,4,24],[11,0,7,31],[20,0,4,36],[17,38,4,20],[0,33,9,31],[23,38,4,13],[0,0,9,31]]}
-];
-
-
-// symbols:
-
-
-
-(lib.CachedTexturedBitmap_1 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(0);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.CachedTexturedBitmap_2 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(1);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.CachedTexturedBitmap_3 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(2);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.CachedTexturedBitmap_6 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(3);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.CachedTexturedBitmap_7 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(4);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.CachedTexturedBitmap_8 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(5);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.CachedTexturedBitmap_9 = function() {
- this.initialize(ss["eq_atlas_"]);
- this.gotoAndStop(6);
-}).prototype = p = new cjs.Sprite();
-
-
-
-(lib.Анимация15 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_9();
- this.instance.parent = this;
- this.instance.setTransform(-0.9,-3.2,0.2092,0.2092);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-3.2,1.9,6.5);
-
-
-(lib.Анимация14 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_8();
- this.instance.parent = this;
- this.instance.setTransform(-0.95,-3.25,0.5,0.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-3.2,2,6.5);
-
-
-(lib.Анимация13 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_7();
- this.instance.parent = this;
- this.instance.setTransform(-0.9,-3.2,0.211,0.211);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-3.2,1.9,6.6);
-
-
-(lib.Анимация12 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_6();
- this.instance.parent = this;
- this.instance.setTransform(-0.95,-5,0.5,0.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-5,2,10);
-
-
-(lib.Анимация11 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_6();
- this.instance.parent = this;
- this.instance.setTransform(-0.95,-5,0.5,0.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-5,2,10);
-
-
-(lib.Анимация10 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_6();
- this.instance.parent = this;
- this.instance.setTransform(-0.95,-5,0.5,0.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-5,2,10);
-
-
-(lib.Анимация7 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_3();
- this.instance.parent = this;
- this.instance.setTransform(-0.95,-9,0.5,0.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-9,2,18);
-
-
-(lib.Анимация4 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_2();
- this.instance.parent = this;
- this.instance.setTransform(-0.9,-4.45,0.289,0.289);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-4.4,2,8.9);
-
-
-(lib.Анимация2 = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // Слой_1
- this.instance = new lib.CachedTexturedBitmap_1();
- this.instance.parent = this;
- this.instance.setTransform(-0.95,-6,0.5,0.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(-0.9,-6,2,12);
-
-
-// stage content:
-(lib.eq = function(mode,startPosition,loop) {
- this.initialize(mode,startPosition,loop,{});
-
- // e
- this.instance = new lib.Анимация2("synched",0);
- this.instance.parent = this;
- this.instance.setTransform(5.95,21.05,1,1,0,0,0,0,6);
-
- this.timeline.addTween(cjs.Tween.get(this.instance).to({scaleY:0.083,y:21.1},29).to({scaleY:1,y:21.05},30).wait(1));
-
- // d
- this.instance_1 = new lib.Анимация4("synched",0);
- this.instance_1.parent = this;
- this.instance_1.setTransform(8.9,21.1,1,0.3611,0,0,0,0,4.5);
-
- this.timeline.addTween(cjs.Tween.get(this.instance_1).to({regY:4.7,scaleY:1.7302,y:21.35},29).to({regY:4.5,scaleY:0.3611,y:21.1},30).wait(1));
-
- // c
- this.instance_2 = new lib.Анимация7("synched",0);
- this.instance_2.parent = this;
- this.instance_2.setTransform(11.85,21.1,1,1,0,0,0,0,9);
-
- this.timeline.addTween(cjs.Tween.get(this.instance_2).to({regX:0.1,regY:9.1,scaleY:0.2417,x:11.95,y:21.15},29).to({regX:0,regY:9,scaleY:1,x:11.85,y:21.1},30).wait(1));
-
- // b
- this.instance_3 = new lib.Анимация10("synched",0);
- this.instance_3.parent = this;
- this.instance_3.setTransform(14.8,21.15,1,0.625,0,0,0,0,5);
-
- this.instance_4 = new lib.Анимация11("synched",0);
- this.instance_4.parent = this;
- this.instance_4.setTransform(14.8,21.1,1,1,0,0,0,0,5);
- this.instance_4._off = true;
-
- this.instance_5 = new lib.Анимация12("synched",0);
- this.instance_5.parent = this;
- this.instance_5.setTransform(14.8,21.15,1,0.62,0,0,0,0,5);
-
- this.timeline.addTween(cjs.Tween.get({}).to({state:[{t:this.instance_3}]}).to({state:[{t:this.instance_4}]},29).to({state:[{t:this.instance_5}]},30).wait(1));
- this.timeline.addTween(cjs.Tween.get(this.instance_3).to({_off:true,scaleY:1,y:21.1},29).wait(31));
- this.timeline.addTween(cjs.Tween.get(this.instance_4).to({_off:false},29).to({_off:true,scaleY:0.62,y:21.15},30).wait(1));
-
- // a
- this.instance_6 = new lib.Анимация13("synched",0);
- this.instance_6.parent = this;
- this.instance_6.setTransform(17.75,21.2,1,2.3692,0,0,0,0,3.3);
-
- this.instance_7 = new lib.Анимация14("synched",0);
- this.instance_7.parent = this;
- this.instance_7.setTransform(17.75,21.15,1,1,0,0,0,0,3.3);
- this.instance_7._off = true;
-
- this.instance_8 = new lib.Анимация15("synched",0);
- this.instance_8.parent = this;
- this.instance_8.setTransform(17.75,21.3,1,2.3897,0,0,0,0,3.3);
-
- this.timeline.addTween(cjs.Tween.get({}).to({state:[{t:this.instance_6}]}).to({state:[{t:this.instance_7}]},29).to({state:[{t:this.instance_8}]},30).wait(1));
- this.timeline.addTween(cjs.Tween.get(this.instance_6).to({_off:true,scaleY:1,y:21.15},29).wait(31));
- this.timeline.addTween(cjs.Tween.get(this.instance_7).to({_off:false},29).to({_off:true,scaleY:2.3897,y:21.3},30).wait(1));
-
-}).prototype = p = new cjs.MovieClip();
-p.nominalBounds = new cjs.Rectangle(17,15.1,1.8000000000000007,6.299999999999999);
-// library properties:
-lib.properties = {
- id: 'AED6681D7DEBBB48AE4A6EEB506702CC',
- width: 24,
- height: 24,
- fps: 60,
- color: "#FFFFFF",
- opacity: 0.00,
- manifest: [
- {src:"images/eq_atlas_.png", id:"eq_atlas_"}
- ],
- preloads: []
-};
-
-
-
-// bootstrap callback support:
-
-(lib.Stage = function(canvas) {
- createjs.Stage.call(this, canvas);
-}).prototype = p = new createjs.Stage();
-
-p.setAutoPlay = function(autoPlay) {
- this.tickEnabled = autoPlay;
-}
-p.play = function() { this.tickEnabled = true; this.getChildAt(0).gotoAndPlay(this.getTimelinePosition()) }
-p.stop = function(ms) { if(ms) this.seek(ms); this.tickEnabled = false; }
-p.seek = function(ms) { this.tickEnabled = true; this.getChildAt(0).gotoAndStop(lib.properties.fps * ms / 1000); }
-p.getDuration = function() { return this.getChildAt(0).totalFrames / lib.properties.fps * 1000; }
-
-p.getTimelinePosition = function() { return this.getChildAt(0).currentFrame / lib.properties.fps * 1000; }
-
-an.bootcompsLoaded = an.bootcompsLoaded || [];
-if(!an.bootstrapListeners) {
- an.bootstrapListeners=[];
-}
-
-an.bootstrapCallback=function(fnCallback) {
- an.bootstrapListeners.push(fnCallback);
- if(an.bootcompsLoaded.length > 0) {
- for(var i=0; i
-
-
- {{ $t('utils.downloadFilesBtn.downloadFiles') }}
-
-
-
- {{ $t('utils.downloadFilesBtn.filesLoaded') }}
-
- {{ filesDownloadProgress }}
-
-
-
- {{ $t('utils.downloadFilesBtn.zippingProgress') }}
- {{ filesZippingProgress }}%
-
-
-
-
-
-
-
-
diff --git a/src/app/css/_reusable.scss b/src/app/css/_reusable.scss
index 74aca39d0..2c0ef0cbb 100644
--- a/src/app/css/_reusable.scss
+++ b/src/app/css/_reusable.scss
@@ -1,5 +1,4 @@
@forward '@webitel/ui-sdk/src/css/main';
-@forward './objects/lib-custom-styling/dropzone';
@forward './objects/history-popup';
// @forward './objects/table-page';
diff --git a/src/app/css/objects/lib-custom-styling/dropzone.scss b/src/app/css/objects/lib-custom-styling/dropzone.scss
deleted file mode 100644
index b859a1e06..000000000
--- a/src/app/css/objects/lib-custom-styling/dropzone.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-#dropzone {
- min-height: 57px;
- margin-bottom: 36px;
- border: 2px dashed var(--primary-color);
- border-radius: var(--border-radius);
-
- &:hover {
- background: #fff;
- }
-
- .dz-message {
- margin: 0;
-
- .dz-custom-message {
- display: flex;
- align-items: center;
- width: fit-content;
- width: -moz-fit-content;
- margin: auto;
-
- .dz-message-text {
- margin-left: 18px;
- }
-
- i, .dz-message-text__accent {
- color: var(--primary-color);
- }
- }
- }
-
- &.dz-started {
- .dz-message {
- display: block;
- }
- }
-
- .dz-preview {
- display: none;
- }
-}
diff --git a/src/app/router/router.ts b/src/app/router/router.ts
index f0c0fade4..cfd1cbdbe 100755
--- a/src/app/router/router.ts
+++ b/src/app/router/router.ts
@@ -23,7 +23,7 @@ import BlacklistsRoutes from '../../modules/lookups/modules/blacklists/router/bl
import BucketsRoutes from '../../modules/lookups/modules/buckets/router/buckets.js';
import CalendarsRoutes from '../../modules/lookups/modules/calendars/router/calendars';
import CommunicationsRoutes from '../../modules/lookups/modules/communications/router/communications.js';
-import MediaRoutes from '../../modules/lookups/modules/media/router/media.js';
+import MediaRoutes from '../../modules/lookups/modules/media/router/media';
import QuickRepliesRoutes from '../../modules/lookups/modules/quick-replies/router/quickReplies.js';
import RegionsRoutes from '../../modules/lookups/modules/regions/router/regions.js';
import ObjectsRoutes from '../../modules/permissions/modules/objects/router/objects.js';
diff --git a/src/main.ts b/src/main.ts
index 9fde61771..dcab58aed 100755
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,3 @@
-// dont know why but when i import dropzone css is css files, it brakes build on firefox (only build!)
-import 'vue2-dropzone/dist/vue2Dropzone.min.css';
import './app/assets/icons/sprite/_index';
import { setConfig as setApiServicesConfig } from '@webitel/api-services';
import { setDefaultAxiosInstance } from '@webitel/api-services/api/axios';
diff --git a/src/modules/lookups/modules/media/components/__tests__/table-media-audio-preview-action.spec.js b/src/modules/lookups/modules/media/components/__tests__/table-media-audio-preview-action.spec.js
deleted file mode 100644
index 1dbd2ede2..000000000
--- a/src/modules/lookups/modules/media/components/__tests__/table-media-audio-preview-action.spec.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import TableMediaAudioPreviewAction from '../table-media-audio-preview-action.vue';
-
-describe('TableMediaAudioPreviewAction', () => {
- it('renders a component', () => {
- const wrapper = shallowMount(TableMediaAudioPreviewAction);
- expect(wrapper.isVisible()).toBe(true);
- });
-});
diff --git a/src/modules/lookups/modules/media/components/__tests__/table-media-file-preview-action.spec.js b/src/modules/lookups/modules/media/components/__tests__/table-media-file-preview-action.spec.js
deleted file mode 100644
index a701f358c..000000000
--- a/src/modules/lookups/modules/media/components/__tests__/table-media-file-preview-action.spec.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import TableMediaFilePreviewAction from '../table-media-file-preview-action.vue';
-
-describe('TableMediaFilePreviewAction', () => {
- it('renders a component', () => {
- const wrapper = shallowMount(TableMediaFilePreviewAction, {
- props: {
- type: '',
- },
- });
- expect(wrapper.isVisible()).toBe(true);
- });
-});
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/components/__tests__/text-to-speech-popup.spec.js b/src/modules/lookups/modules/media/components/__tests__/text-to-speech-popup.spec.js
similarity index 85%
rename from src/modules/lookups/modules/media/modules/text-to-speech/components/__tests__/text-to-speech-popup.spec.js
rename to src/modules/lookups/modules/media/components/__tests__/text-to-speech-popup.spec.js
index 912adcd24..6a60eb218 100644
--- a/src/modules/lookups/modules/media/modules/text-to-speech/components/__tests__/text-to-speech-popup.spec.js
+++ b/src/modules/lookups/modules/media/components/__tests__/text-to-speech-popup.spec.js
@@ -5,6 +5,6 @@ import TextToSpeechPopup from '../text-to-speech-popup.vue';
describe('TextToSpeechPopup', () => {
it('renders a component', () => {
const wrapper = shallowMount(TextToSpeechPopup);
- expect(wrapper.isVisible()).toBe(true);
+ expect(wrapper.exists()).toBe(true);
});
});
diff --git a/src/modules/lookups/modules/media/components/download-media-files-action.vue b/src/modules/lookups/modules/media/components/download-media-files-action.vue
new file mode 100644
index 000000000..3c1dd4b19
--- /dev/null
+++ b/src/modules/lookups/modules/media/components/download-media-files-action.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/src/modules/lookups/modules/media/components/table-media-audio-preview-action.vue b/src/modules/lookups/modules/media/components/table-media-audio-preview-action.vue
deleted file mode 100644
index 62d9b02ec..000000000
--- a/src/modules/lookups/modules/media/components/table-media-audio-preview-action.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/lookups/modules/media/components/table-media-file-preview-action.vue b/src/modules/lookups/modules/media/components/table-media-file-preview-action.vue
deleted file mode 100644
index df7234d75..000000000
--- a/src/modules/lookups/modules/media/components/table-media-file-preview-action.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/modules/lookups/modules/media/components/text-to-speech-popup.vue b/src/modules/lookups/modules/media/components/text-to-speech-popup.vue
new file mode 100644
index 000000000..d45926a4f
--- /dev/null
+++ b/src/modules/lookups/modules/media/components/text-to-speech-popup.vue
@@ -0,0 +1,271 @@
+
+
+
+ {{ t('reusable.tts') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/lookups/modules/media/components/the-media.vue b/src/modules/lookups/modules/media/components/the-media.vue
index 9d186bdeb..a0646face 100644
--- a/src/modules/lookups/modules/media/components/the-media.vue
+++ b/src/modules/lookups/modules/media/components/the-media.vue
@@ -1,23 +1,29 @@
-
+
+
+
+
-
-
-
-
-
{{ loadedCount }}/{{ allLoadingCount }}
-
-
-
+
+
-
-
- {{ $t('objects.lookups.media.dragPlaceholder') }}
+
+
+ {{ t('objects.lookups.media.dragPlaceholder') }}
- {{ $t('objects.lookups.media.dragPlaceholderHere') }}
-
-
-
+ {{ t('objects.lookups.media.dragPlaceholderHere') }}
+
+
+
+
+
+
+
+
-
-
-
{{ item.name }}
- {{ prettifyDate(item.createdAt) }}
+ {{ formatDate(Number(item.createdAt), FormatDateMode.DATE) }}
- {{ displayFormatFile(item) }}
+ {{ getMediaFileFormat(item) }}
{{ prettifyFileSize(item.size) }}
-
-
+
+
deleteData(item),
- })"
+ @click="
+ askDeleteConfirmation({
+ deleted: [item],
+ callback: () => deleteEls([item]),
+ })
+ "
/>
+
-
-
diff --git a/src/modules/lookups/modules/media/composables/__tests__/useMediaPlayer.spec.ts b/src/modules/lookups/modules/media/composables/__tests__/useMediaPlayer.spec.ts
new file mode 100644
index 000000000..1b1504f02
--- /dev/null
+++ b/src/modules/lookups/modules/media/composables/__tests__/useMediaPlayer.spec.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from 'vitest';
+
+import { useMediaPlayer } from '../useMediaPlayer';
+
+const first = {
+ id: '1',
+ mimeType: 'audio/wav',
+};
+const second = {
+ id: '2',
+ mimeType: 'audio/mpeg',
+};
+
+describe('useMediaPlayer', () => {
+ it('streams the chosen file and marks it as playing', () => {
+ const player = useMediaPlayer();
+
+ player.play(second);
+
+ expect(player.playerSrc.value).toEqual({
+ src: expect.stringContaining('/storage/media/2/stream'),
+ type: 'audio/mpeg',
+ });
+ expect(player.isPlaying(second)).toBe(true);
+ expect(player.isPlaying(first)).toBe(false);
+ });
+
+ it('forgets the file on close', () => {
+ const player = useMediaPlayer();
+
+ player.play(first);
+ player.closePlayer();
+
+ expect(player.playerSrc.value).toBeNull();
+ expect(player.isPlaying(first)).toBe(false);
+ });
+});
diff --git a/src/modules/lookups/modules/media/composables/__tests__/useMediaUpload.spec.ts b/src/modules/lookups/modules/media/composables/__tests__/useMediaUpload.spec.ts
new file mode 100644
index 000000000..6ea204ba4
--- /dev/null
+++ b/src/modules/lookups/modules/media/composables/__tests__/useMediaUpload.spec.ts
@@ -0,0 +1,87 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { useMediaUpload } from '../useMediaUpload';
+
+const { uploadMedia, handlers } = vi.hoisted(() => ({
+ uploadMedia: vi.fn(),
+ handlers: {} as {
+ drop?: (files: File[] | null) => Promise
;
+ pick?: (files: FileList | null) => Promise;
+ },
+}));
+
+vi.mock('@webitel/api-services/api', () => ({
+ MediaAPI: {
+ upload: uploadMedia,
+ },
+}));
+
+vi.mock('@vueuse/core', async () => {
+ const { shallowRef } = await import('vue');
+ return {
+ useDropZone: (_target: unknown, { onDrop }) => {
+ handlers.drop = onDrop;
+ return {
+ isOverDropZone: shallowRef(false),
+ };
+ },
+ useFileDialog: () => ({
+ open: vi.fn(),
+ onChange: (callback) => {
+ handlers.pick = callback;
+ },
+ }),
+ };
+});
+
+const files = [
+ new File([], 'a.wav'),
+ new File([], 'b.wav'),
+];
+
+describe('useMediaUpload', () => {
+ beforeEach(() => {
+ uploadMedia.mockReset();
+ });
+
+ it('uploads dropped files in one request and reloads the list', async () => {
+ uploadMedia.mockResolvedValue({});
+ const onUploaded = vi.fn();
+ const upload = useMediaUpload({
+ onUploaded,
+ });
+
+ const pending = handlers.drop?.(files);
+ expect(upload.isUploading.value).toBe(true);
+ await pending;
+
+ expect(uploadMedia).toHaveBeenCalledWith(files);
+ expect(upload.isUploading.value).toBe(false);
+ expect(onUploaded).toHaveBeenCalledOnce();
+ });
+
+ it('reloads the list even when the upload fails', async () => {
+ uploadMedia.mockRejectedValue(new Error('Too big'));
+ const onUploaded = vi.fn();
+ const upload = useMediaUpload({
+ onUploaded,
+ });
+
+ await handlers.pick?.(files as unknown as FileList);
+
+ expect(upload.isUploading.value).toBe(false);
+ expect(onUploaded).toHaveBeenCalledOnce();
+ });
+
+ it('ignores an empty pick', async () => {
+ const onUploaded = vi.fn();
+ useMediaUpload({
+ onUploaded,
+ });
+
+ await handlers.pick?.(null);
+
+ expect(uploadMedia).not.toHaveBeenCalled();
+ expect(onUploaded).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/modules/lookups/modules/media/composables/useMediaPlayer.ts b/src/modules/lookups/modules/media/composables/useMediaPlayer.ts
new file mode 100644
index 000000000..cacd4287a
--- /dev/null
+++ b/src/modules/lookups/modules/media/composables/useMediaPlayer.ts
@@ -0,0 +1,38 @@
+import { getMediaStreamUrl } from '@webitel/api-services/api';
+import type { StorageMediaFile } from '@webitel/api-services/gen/models';
+import { computed, ref } from 'vue';
+import { getMediaUrlAuth } from '../scripts/getMediaUrlAuth';
+
+/** [Claude] the bottom player: which file it plays */
+export function useMediaPlayer() {
+ const playingFile = ref(null);
+
+ const playerSrc = computed(
+ () =>
+ playingFile.value && {
+ src: getMediaStreamUrl(playingFile.value.id ?? '', {
+ auth: getMediaUrlAuth(),
+ }),
+ type: playingFile.value.mimeType ?? '',
+ },
+ );
+
+ const play = (file: StorageMediaFile) => {
+ playingFile.value = file;
+ };
+
+ const closePlayer = () => {
+ playingFile.value = null;
+ };
+
+ /** [Claude] true while the file is in the player, paused or not — its row shows stop, as in history */
+ const isPlaying = (file: StorageMediaFile) =>
+ file.id === playingFile.value?.id;
+
+ return {
+ playerSrc,
+ play,
+ closePlayer,
+ isPlaying,
+ };
+}
diff --git a/src/modules/lookups/modules/media/composables/useMediaUpload.ts b/src/modules/lookups/modules/media/composables/useMediaUpload.ts
new file mode 100644
index 000000000..2587d8e40
--- /dev/null
+++ b/src/modules/lookups/modules/media/composables/useMediaUpload.ts
@@ -0,0 +1,44 @@
+import { useDropZone, useFileDialog } from '@vueuse/core';
+import { MediaAPI } from '@webitel/api-services/api';
+import { ref } from 'vue';
+
+/** [Claude] drop or pick files into `dropZone`; `onUploaded` fires after the request, failed or not */
+export function useMediaUpload({ onUploaded }: { onUploaded: () => void }) {
+ const dropZone = ref();
+ const isUploading = ref(false);
+
+ const upload = async (files: File[] | FileList | null) => {
+ if (!files?.length || isUploading.value) return;
+
+ isUploading.value = true;
+ try {
+ await MediaAPI.upload(Array.from(files));
+ } catch {
+ // [Claude] MediaAPI has already notified; drop/dialog callers don't await, so don't rethrow
+ } finally {
+ isUploading.value = false;
+ onUploaded();
+ }
+ };
+
+ const { isOverDropZone } = useDropZone(dropZone, {
+ onDrop: upload,
+ });
+
+ const { open, onChange } = useFileDialog({
+ multiple: true,
+ reset: true,
+ });
+ onChange(upload);
+
+ const openFileDialog = () => {
+ if (!isUploading.value) open();
+ };
+
+ return {
+ dropZone,
+ isOverDropZone,
+ isUploading,
+ openFileDialog,
+ };
+}
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/api/TextToSpeechAPI.js b/src/modules/lookups/modules/media/modules/text-to-speech/api/TextToSpeechAPI.js
deleted file mode 100644
index ca9981bd3..000000000
--- a/src/modules/lookups/modules/media/modules/text-to-speech/api/TextToSpeechAPI.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import applyTransform, {
- camelToSnake,
- generateUrl,
- notify,
-} from '@webitel/ui-sdk/src/api/transformers/index.js';
-
-import instance from '../../../../../../../app/api/instance';
-
-const getTtsStreamUrl = (params, apiUrl = false) => {
- const baseUrl = '/storage/tts/stream';
- let url = applyTransform(params, [
- (params) => ({
- ...params,
- access_token: instance.defaults.headers['X-Webitel-Access'],
- }),
- camelToSnake(),
- generateUrl(baseUrl),
- ]);
- if (apiUrl) url = `${import.meta.env.VITE_API_URL}${url}`;
- return url;
-};
-
-const getTts = async (params) => {
- const url = getTtsStreamUrl(params, true);
-
- try {
- const response = await fetch(url);
- if (!response.ok) throw new Error(response.status);
- return response.blob();
- } catch (err) {
- throw applyTransform(err, [
- notify(({ callback }) =>
- callback({
- type: 'error',
- text: `Failed to process Text-to-Speech: ${err}`,
- }),
- ),
- ]);
- }
-};
-
-const TextToSpeechAPI = {
- getTtsStreamUrl,
- getTts,
-};
-
-export default TextToSpeechAPI;
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/components/text-to-speech-popup.vue b/src/modules/lookups/modules/media/modules/text-to-speech/components/text-to-speech-popup.vue
deleted file mode 100644
index e362f5e35..000000000
--- a/src/modules/lookups/modules/media/modules/text-to-speech/components/text-to-speech-popup.vue
+++ /dev/null
@@ -1,309 +0,0 @@
-
-
-
-
-
-
- {{ $t('reusable.tts') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsGoogleVoice.enum.js b/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsGoogleVoice.enum.js
deleted file mode 100644
index baccf7393..000000000
--- a/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsGoogleVoice.enum.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- *@author r.zaritskyi
- *
- * Task WTEL-9674 states that this task is intended to update only
- * Google Text-to-Speech voice profiles for the Ukrainian language.
- * If necessary, voices for other languages can be added
- */
-const TtsGoogleVoice = Object.freeze([
- 'uk-UA-Chirp3-HD-Achernar',
- 'uk-UA-Chirp3-HD-Achird',
- 'uk-UA-Chirp3-HD-Algenib',
- 'uk-UA-Chirp3-HD-Algieba',
- 'uk-UA-Chirp3-HD-Alnilam',
- 'uk-UA-Chirp3-HD-Autonoe',
- 'uk-UA-Chirp3-HD-Callirrhoe',
- 'uk-UA-Chirp3-HD-Despina',
- 'uk-UA-Chirp3-HD-Enceladus',
- 'uk-UA-Chirp3-HD-Erinome',
- 'uk-UA-Chirp3-HD-Gacrux',
- 'uk-UA-Chirp3-HD-Iapetus',
- 'uk-UA-Chirp3-HD-Laomedeia',
- 'uk-UA-Chirp3-HD-Pulcherrima',
- 'uk-UA-Chirp3-HD-Rasalgethi',
- 'uk-UA-Chirp3-HD-Sadachbia',
- 'uk-UA-Chirp3-HD-Sadaltager',
- 'uk-UA-Chirp3-HD-Schedar',
- 'uk-UA-Chirp3-HD-Sulafat',
- 'uk-UA-Chirp3-HD-Umbriel',
- 'uk-UA-Chirp3-HD-Vindemiatrix',
- 'uk-UA-Chirp3-HD-Zubenelgenubi',
- 'uk-UA-Standard-B',
- 'uk-UA-Wavenet-B',
- 'uk-UA-Wavenet-A',
-]);
-
-export default TtsGoogleVoice;
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsMicrosoftVoice.enum.js b/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsMicrosoftVoice.enum.js
deleted file mode 100644
index 312f38e48..000000000
--- a/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsMicrosoftVoice.enum.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const TtsMicrosoftVoice = Object.freeze([
- 'Male',
- 'Female',
-]);
-
-export default TtsMicrosoftVoice;
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsTextType.enum.js b/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsTextType.enum.js
deleted file mode 100644
index efee7744c..000000000
--- a/src/modules/lookups/modules/media/modules/text-to-speech/enums/TtsTextType.enum.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const TtsTextType = Object.freeze({
- SSML: 'ssml',
- TEXT: 'text',
-});
-
-export default TtsTextType;
diff --git a/src/modules/lookups/modules/media/modules/text-to-speech/lookups/TtsTextType.lookup.js b/src/modules/lookups/modules/media/modules/text-to-speech/lookups/TtsTextType.lookup.js
deleted file mode 100644
index 4c734b932..000000000
--- a/src/modules/lookups/modules/media/modules/text-to-speech/lookups/TtsTextType.lookup.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import TtsTextTypeEnum from '../enums/TtsTextType.enum';
-
-const TtsTextType = Object.freeze([
- {
- locale: 'vocabulary.text',
- value: TtsTextTypeEnum.TEXT,
- },
- {
- locale: 'objects.lookups.media.tts.textType.ssml',
- value: TtsTextTypeEnum.SSML,
- },
-]);
-
-export default TtsTextType;
diff --git a/src/modules/lookups/modules/media/router/media.js b/src/modules/lookups/modules/media/router/media.ts
similarity index 95%
rename from src/modules/lookups/modules/media/router/media.js
rename to src/modules/lookups/modules/media/router/media.ts
index f7d6834e8..3bdb90a1d 100644
--- a/src/modules/lookups/modules/media/router/media.js
+++ b/src/modules/lookups/modules/media/router/media.ts
@@ -1,6 +1,6 @@
import { AdminSections, WtObject } from '@webitel/ui-sdk/enums';
-import RouteNames from '../../../../../app/router/_internals/RouteNames.enum.js';
+import RouteNames from '../../../../../app/router/_internals/RouteNames.enum';
const Media = () => import('../components/the-media.vue');
@@ -15,4 +15,5 @@ const MediaRoutes = [
},
},
];
+
export default MediaRoutes;
diff --git a/src/modules/lookups/modules/media/scripts/getMediaUrlAuth.ts b/src/modules/lookups/modules/media/scripts/getMediaUrlAuth.ts
new file mode 100644
index 000000000..414dbb72a
--- /dev/null
+++ b/src/modules/lookups/modules/media/scripts/getMediaUrlAuth.ts
@@ -0,0 +1,7 @@
+import type { MediaUrlAuth } from '@webitel/api-services/api';
+
+/** [Claude] api-services media URL helpers take the API url and token from the app; read on every call, as the token changes after login */
+export const getMediaUrlAuth = (): MediaUrlAuth => ({
+ baseUrl: import.meta.env.VITE_API_URL,
+ accessToken: localStorage.getItem('access-token') || '',
+});
diff --git a/src/modules/lookups/modules/media/store/media.js b/src/modules/lookups/modules/media/store/media.js
deleted file mode 100644
index 68f74535e..000000000
--- a/src/modules/lookups/modules/media/store/media.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { MediaAPI } from '@webitel/api-services/api';
-import ObjectStoreModule from '../../../../../app/store/BaseStoreModules/StoreModules/ObjectStoreModule';
-import headers from './_internals/headers';
-
-const media = new ObjectStoreModule({
- headers,
-})
- .attachAPIModule(MediaAPI)
- .generateAPIActions()
- .getModule();
-
-export default media;
diff --git a/src/modules/lookups/modules/media/store/_internals/headers.js b/src/modules/lookups/modules/media/stores/datalist/_internals/headers.ts
similarity index 75%
rename from src/modules/lookups/modules/media/store/_internals/headers.js
rename to src/modules/lookups/modules/media/stores/datalist/_internals/headers.ts
index 435974426..57e492101 100644
--- a/src/modules/lookups/modules/media/store/_internals/headers.js
+++ b/src/modules/lookups/modules/media/stores/datalist/_internals/headers.ts
@@ -1,28 +1,33 @@
+import type { DatalistTableHeader } from '@webitel/ui-datalist';
import { SortSymbols } from '@webitel/ui-sdk/src/scripts/sortQueryAdapters';
-export default [
+export const headers: DatalistTableHeader[] = [
{
value: 'name',
locale: 'objects.name',
field: 'name',
+ show: true,
sort: SortSymbols.NONE,
},
{
value: 'createdAt',
locale: 'objects.lookups.media.createdAt',
field: 'created_at',
+ show: true,
sort: SortSymbols.NONE,
},
{
value: 'format',
locale: 'objects.lookups.media.format',
field: 'mime_type',
+ show: true,
sort: SortSymbols.NONE,
},
{
value: 'size',
locale: 'objects.lookups.media.size',
field: 'size',
+ show: true,
sort: SortSymbols.NONE,
},
];
diff --git a/src/modules/lookups/modules/media/stores/datalist/mediaDatalistStore.ts b/src/modules/lookups/modules/media/stores/datalist/mediaDatalistStore.ts
new file mode 100644
index 000000000..ffaba996f
--- /dev/null
+++ b/src/modules/lookups/modules/media/stores/datalist/mediaDatalistStore.ts
@@ -0,0 +1,13 @@
+import { MediaAPI } from '@webitel/api-services/api';
+import { createTableStore } from '@webitel/ui-datalist';
+
+import { MediaNamespace } from '../namespace';
+import { headers } from './_internals/headers';
+
+export const useMediaDatalistStore = createTableStore(
+ `${MediaNamespace}/datalist`,
+ {
+ apiModule: MediaAPI,
+ headers,
+ },
+);
diff --git a/src/modules/lookups/modules/media/stores/namespace.ts b/src/modules/lookups/modules/media/stores/namespace.ts
new file mode 100644
index 000000000..6d00f1155
--- /dev/null
+++ b/src/modules/lookups/modules/media/stores/namespace.ts
@@ -0,0 +1,3 @@
+import { AdminNamespace } from '../../../../../app/namespace';
+
+export const MediaNamespace = `${AdminNamespace}/lookups/media`;
diff --git a/src/modules/lookups/store/lookups.js b/src/modules/lookups/store/lookups.js
index a445db348..4f9be13a3 100644
--- a/src/modules/lookups/store/lookups.js
+++ b/src/modules/lookups/store/lookups.js
@@ -1,7 +1,6 @@
import skills from '../modules/agent-skills/store/agent-skills';
import blacklists from '../modules/blacklists/store/blacklists';
import communications from '../modules/communications/store/communications';
-import media from '../modules/media/store/media';
import quickReplies from '../modules/quick-replies/store/quick-replies.js';
import regions from '../modules/regions/store/regions';
@@ -10,7 +9,6 @@ const modules = {
blacklists,
regions,
communications,
- media,
quickReplies,
};