@necrolab/dashboard 0.5.28 → 0.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/api.js +7 -5
- package/backend/batching.js +59 -2
- package/backend/index.js +1 -1
- package/index.html +10 -23
- package/package.json +1 -1
- package/src/assets/css/base/scroll.scss +1 -1
- package/src/assets/css/main.scss +14 -14
- package/src/components/Console/ConsoleToolbar.vue +8 -8
- package/src/components/Editors/Account/Account.vue +9 -5
- package/src/components/Editors/Account/AccountView.vue +37 -18
- package/src/components/Editors/Account/CreateAccount.vue +38 -4
- package/src/components/Editors/Profile/CreateProfile.vue +29 -4
- package/src/components/Editors/Profile/Profile.vue +11 -6
- package/src/components/Editors/Profile/ProfileCountryChooser.vue +2 -2
- package/src/components/Editors/Profile/ProfileView.vue +37 -18
- package/src/components/Tasks/CreateTaskAXS.vue +16 -2
- package/src/components/Tasks/CreateTaskTM.vue +28 -5
- package/src/components/Tasks/QuickSettings.vue +77 -10
- package/src/components/Tasks/Task.vue +20 -7
- package/src/components/Tasks/TaskView.vue +144 -58
- package/src/components/Tasks/ViewTask.vue +17 -3
- package/src/components/ui/Modal.vue +1 -1
- package/src/components/ui/ReadonlyFieldsSection.vue +3 -3
- package/src/components/ui/StatusBadge.vue +1 -1
- package/src/components/ui/TaskToggle.vue +2 -3
- package/src/components/ui/controls/CountryChooser.vue +2 -2
- package/src/components/ui/controls/atomic/Dropdown.vue +2 -2
- package/src/components/ui/controls/atomic/MultiDropdown.vue +1 -1
- package/src/composables/useDynamicTableHeight.js +4 -4
- package/src/composables/useRowSelection.js +0 -1
- package/src/composables/useZoomPrevention.js +16 -55
- package/src/stores/connection.js +453 -68
- package/src/stores/sampleData.js +34 -24
- package/src/stores/ui.js +89 -100
- package/src/views/Accounts.vue +2 -5
- package/src/views/Console.vue +13 -14
- package/src/views/Profiles.vue +2 -5
- package/src/views/Tasks.vue +29 -1
- package/vite.config.js +4 -2
package/backend/api.js
CHANGED
|
@@ -20,8 +20,10 @@ import { users } from "./mock-data.js";
|
|
|
20
20
|
|
|
21
21
|
const logger = createLogger("WEB UI");
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const TASK_BATCH_WAIT_TIME = 120; // ms
|
|
24
|
+
const TASK_BATCH_MAX_MESSAGES = 250;
|
|
25
|
+
const CONSOLE_BATCH_WAIT_TIME = 250; // ms
|
|
26
|
+
const CONSOLE_BATCH_MAX_MESSAGES = 120;
|
|
25
27
|
|
|
26
28
|
const app = express();
|
|
27
29
|
const auth = new authSystem();
|
|
@@ -97,8 +99,8 @@ const pushTaskWSUpdate = (batch) => {
|
|
|
97
99
|
});
|
|
98
100
|
};
|
|
99
101
|
|
|
100
|
-
const taskBatcher = new Batcher(pushTaskWSUpdate,
|
|
101
|
-
const logBatcher = new Batcher(pushConsoleWSUpdate,
|
|
102
|
+
const taskBatcher = new Batcher(pushTaskWSUpdate, TASK_BATCH_MAX_MESSAGES, TASK_BATCH_WAIT_TIME);
|
|
103
|
+
const logBatcher = new Batcher(pushConsoleWSUpdate, CONSOLE_BATCH_MAX_MESSAGES, CONSOLE_BATCH_WAIT_TIME);
|
|
102
104
|
|
|
103
105
|
const pushWSUpdate = (u) => {
|
|
104
106
|
if (u.type === "console") logBatcher.add(u);
|
|
@@ -270,7 +272,7 @@ app.post("/api/json-file", async (req, res) => {
|
|
|
270
272
|
|
|
271
273
|
try {
|
|
272
274
|
if (requestedFile.endsWith(".json")) JSON.parse(content);
|
|
273
|
-
} catch
|
|
275
|
+
} catch {
|
|
274
276
|
return res.status(400).send({
|
|
275
277
|
error: "Content must be valid JSON"
|
|
276
278
|
});
|
package/backend/batching.js
CHANGED
|
@@ -10,11 +10,68 @@ class Batcher {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
add(msg) {
|
|
13
|
-
if (msg
|
|
14
|
-
|
|
13
|
+
if (!msg) return;
|
|
14
|
+
|
|
15
|
+
const isTaskUpdate =
|
|
16
|
+
msg.event === "task-update" || msg.event === "task-setstatus" || msg.event === "task-setinfo";
|
|
17
|
+
|
|
18
|
+
if (isTaskUpdate) {
|
|
19
|
+
const taskId = msg.id || msg.task?.taskId;
|
|
20
|
+
if (taskId) {
|
|
21
|
+
const index = this.inMessages.findIndex((queued) => {
|
|
22
|
+
const queuedIsTaskUpdate =
|
|
23
|
+
queued.event === "task-update" ||
|
|
24
|
+
queued.event === "task-setstatus" ||
|
|
25
|
+
queued.event === "task-setinfo";
|
|
26
|
+
const queuedTaskId = queued.id || queued.task?.taskId;
|
|
27
|
+
return queuedIsTaskUpdate && queuedTaskId === taskId && queued.event === msg.event;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (index !== -1) {
|
|
31
|
+
this.inMessages[index] = this.mergeTaskMessages(this.inMessages[index], msg, taskId);
|
|
32
|
+
return this.sendIfReady();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else if (msg.type === "console" && !msg.log) return;
|
|
15
36
|
|
|
16
37
|
this.inMessages.push(msg);
|
|
38
|
+
this.sendIfReady();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
mergeTaskMessages(existing, incoming, taskId) {
|
|
42
|
+
if (!existing) return { ...incoming, id: incoming.id || taskId };
|
|
43
|
+
if (incoming?.task?.removed) return { ...incoming, id: incoming.id || taskId };
|
|
44
|
+
if (existing?.task?.removed) return existing;
|
|
45
|
+
|
|
46
|
+
const merged = {
|
|
47
|
+
...existing,
|
|
48
|
+
...incoming,
|
|
49
|
+
id: incoming.id || existing.id || taskId
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (existing.task || incoming.task) {
|
|
53
|
+
merged.task = {
|
|
54
|
+
...(existing.task || {}),
|
|
55
|
+
...(incoming.task || {})
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (existing.update || incoming.update) {
|
|
60
|
+
merged.update = {
|
|
61
|
+
...(existing.update || {}),
|
|
62
|
+
...(incoming.update || {})
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (merged.task && merged.update) {
|
|
67
|
+
Object.assign(merged.task, merged.update);
|
|
68
|
+
delete merged.update;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
17
73
|
|
|
74
|
+
sendIfReady() {
|
|
18
75
|
// Enough messages for new batch
|
|
19
76
|
if (this.inMessages.length >= this.maxSize) {
|
|
20
77
|
this.lastBatchSent = Date.now();
|
package/backend/index.js
CHANGED
package/index.html
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" href="/favicon.ico" />
|
|
6
6
|
<meta
|
|
7
7
|
name="viewport"
|
|
8
|
-
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
9
|
-
/>
|
|
8
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
10
9
|
<!-- Lock iPhone to portrait orientation only -->
|
|
11
10
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
12
11
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
@@ -24,7 +23,8 @@
|
|
|
24
23
|
color-scheme: dark only;
|
|
25
24
|
-webkit-color-scheme: dark;
|
|
26
25
|
}
|
|
27
|
-
html,
|
|
26
|
+
html,
|
|
27
|
+
body {
|
|
28
28
|
color-scheme: dark only !important;
|
|
29
29
|
}
|
|
30
30
|
</style>
|
|
@@ -38,21 +38,11 @@
|
|
|
38
38
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
39
39
|
<link
|
|
40
40
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
41
|
-
rel="stylesheet"
|
|
42
|
-
/>
|
|
43
|
-
|
|
44
|
-
<!-- Preload critical fonts -->
|
|
45
|
-
<link
|
|
46
|
-
rel="preload"
|
|
47
|
-
href="https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2"
|
|
48
|
-
as="font"
|
|
49
|
-
type="font/woff2"
|
|
50
|
-
crossorigin
|
|
51
|
-
/>
|
|
41
|
+
rel="stylesheet" />
|
|
52
42
|
|
|
53
|
-
<!--
|
|
54
|
-
<link rel="
|
|
55
|
-
<link rel="
|
|
43
|
+
<!-- Prefetch likely-used assets -->
|
|
44
|
+
<link rel="prefetch" as="image" href="/img/logo_trans.png" />
|
|
45
|
+
<link rel="prefetch" as="image" href="/img/reconnect-logo.png" />
|
|
56
46
|
|
|
57
47
|
<!-- Prefetch core navigation icons -->
|
|
58
48
|
<link rel="prefetch" as="image" href="/img/close.svg" />
|
|
@@ -112,17 +102,14 @@
|
|
|
112
102
|
<!-- Prefetch PWA resources -->
|
|
113
103
|
<link rel="prefetch" as="script" href="/sw.js" />
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
<link rel="preload" as="fetch" href="/manifest.json" crossorigin />
|
|
117
|
-
<link rel="manifest" href="/manifest.json?v=3" />
|
|
105
|
+
<link rel="manifest" href="/manifest.json?v=3" crossorigin />
|
|
118
106
|
<meta name="theme-color" content="#1a1b1e" />
|
|
119
107
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=3" />
|
|
120
108
|
|
|
121
109
|
<!-- Prism.js for syntax highlighting -->
|
|
122
110
|
<link
|
|
123
111
|
rel="stylesheet"
|
|
124
|
-
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"
|
|
125
|
-
/>
|
|
112
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
|
|
126
113
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
|
127
114
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
|
128
115
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
package/package.json
CHANGED
package/src/assets/css/main.scss
CHANGED
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
Modular SCSS architecture with @use imports
|
|
4
4
|
========================================================================== */
|
|
5
5
|
|
|
6
|
-
@use "base/reset";
|
|
7
|
-
@use "base/scroll";
|
|
8
|
-
@use "base/typography";
|
|
9
|
-
@use "base/variables";
|
|
10
|
-
@use "base/mixins" as *;
|
|
11
|
-
@use "components/buttons";
|
|
12
|
-
@use "components/forms";
|
|
13
|
-
@use "components/toasts";
|
|
14
|
-
@use "components/modals";
|
|
15
|
-
@use "components/tables";
|
|
16
|
-
@use "components/search-groups";
|
|
17
|
-
@use "components/headers";
|
|
18
|
-
@use "components/utilities";
|
|
19
|
-
@use "components/accessibility";
|
|
6
|
+
@use "./base/reset";
|
|
7
|
+
@use "./base/scroll";
|
|
8
|
+
@use "./base/typography";
|
|
9
|
+
@use "./base/variables";
|
|
10
|
+
@use "./base/mixins" as *;
|
|
11
|
+
@use "./components/buttons";
|
|
12
|
+
@use "./components/forms";
|
|
13
|
+
@use "./components/toasts";
|
|
14
|
+
@use "./components/modals";
|
|
15
|
+
@use "./components/tables";
|
|
16
|
+
@use "./components/search-groups";
|
|
17
|
+
@use "./components/headers";
|
|
18
|
+
@use "./components/utilities";
|
|
19
|
+
@use "./components/accessibility";
|
|
20
20
|
|
|
21
21
|
/* ==========================================================================
|
|
22
22
|
BODY LAYOUT & BACKGROUND
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
@mousedown="$emit('scroll', 'up')"
|
|
29
29
|
@mouseup="$emit('scroll-stop')"
|
|
30
30
|
@mouseleave="$emit('scroll-stop')"
|
|
31
|
-
@touchstart="$emit('scroll', 'up')"
|
|
32
|
-
@touchend="$emit('scroll-stop')"
|
|
31
|
+
@touchstart.passive="$emit('scroll', 'up')"
|
|
32
|
+
@touchend.passive="$emit('scroll-stop')"
|
|
33
33
|
aria-label="Scroll up">
|
|
34
34
|
<UpIcon class="pointer-events-none h-5 w-5" />
|
|
35
35
|
</button>
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
@mousedown="$emit('scroll', 'down')"
|
|
39
39
|
@mouseup="$emit('scroll-stop')"
|
|
40
40
|
@mouseleave="$emit('scroll-stop')"
|
|
41
|
-
@touchstart="$emit('scroll', 'down')"
|
|
42
|
-
@touchend="$emit('scroll-stop')">
|
|
41
|
+
@touchstart.passive="$emit('scroll', 'down')"
|
|
42
|
+
@touchend.passive="$emit('scroll-stop')">
|
|
43
43
|
<DownIcon class="pointer-events-none h-5 w-5" />
|
|
44
44
|
</button>
|
|
45
45
|
</div>
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
@mousedown="$emit('scroll', 'up')"
|
|
65
65
|
@mouseup="$emit('scroll-stop')"
|
|
66
66
|
@mouseleave="$emit('scroll-stop')"
|
|
67
|
-
@touchstart="$emit('scroll', 'up')"
|
|
68
|
-
@touchend="$emit('scroll-stop')">
|
|
67
|
+
@touchstart.passive="$emit('scroll', 'up')"
|
|
68
|
+
@touchend.passive="$emit('scroll-stop')">
|
|
69
69
|
<UpIcon class="pointer-events-none h-5 w-5" />
|
|
70
70
|
</button>
|
|
71
71
|
<button
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
@mousedown="$emit('scroll', 'down')"
|
|
74
74
|
@mouseup="$emit('scroll-stop')"
|
|
75
75
|
@mouseleave="$emit('scroll-stop')"
|
|
76
|
-
@touchstart="$emit('scroll', 'down')"
|
|
77
|
-
@touchend="$emit('scroll-stop')">
|
|
76
|
+
@touchstart.passive="$emit('scroll', 'down')"
|
|
77
|
+
@touchend.passive="$emit('scroll-stop')">
|
|
78
78
|
<DownIcon class="pointer-events-none h-5 w-5" />
|
|
79
79
|
</button>
|
|
80
80
|
</div>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
@click="ui.setOpenContextMenu('')"
|
|
5
5
|
@click.right.prevent="ui.setOpenContextMenu('')"
|
|
6
6
|
@dblclick="handleDoubleClick"
|
|
7
|
-
@touchstart="handleTouchStart"
|
|
8
|
-
@touchend="handleTouchEnd">
|
|
7
|
+
@touchstart.passive="handleTouchStart"
|
|
8
|
+
@touchend.passive="handleTouchEnd">
|
|
9
9
|
<div class="col-span-3 flex lg:col-span-2">
|
|
10
10
|
<Checkbox
|
|
11
11
|
class="ml-0 mr-4"
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
</div>
|
|
18
18
|
<div class="col-span-2 hidden md:block" @click="copy(props.account.password, 'Copied password')">
|
|
19
19
|
<h4 class="text-center text-white">
|
|
20
|
-
{{ props.
|
|
20
|
+
{{ props.privacy ? "•".repeat(props.account.password.length) : props.account.password }}
|
|
21
21
|
</h4>
|
|
22
22
|
</div>
|
|
23
23
|
<div class="col-span-1 flex justify-center">
|
|
24
|
-
<StatusBadge :enabled="props.account.enabled" size="small" />
|
|
24
|
+
<StatusBadge :enabled="Boolean(props.account.enabled)" size="small" />
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<div class="col-span-1 hidden lg:block">
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<EditIcon />
|
|
38
38
|
</button>
|
|
39
39
|
</li>
|
|
40
|
-
<li v-if="props.account.enabled">
|
|
40
|
+
<li v-if="Boolean(props.account.enabled)">
|
|
41
41
|
<button @click="disable">
|
|
42
42
|
<img class="icon-md" src="/img/controls/disable.svg" />
|
|
43
43
|
</button>
|
|
@@ -69,6 +69,10 @@ const props = defineProps({
|
|
|
69
69
|
account: {
|
|
70
70
|
type: Object,
|
|
71
71
|
required: true
|
|
72
|
+
},
|
|
73
|
+
privacy: {
|
|
74
|
+
type: Boolean,
|
|
75
|
+
default: true
|
|
72
76
|
}
|
|
73
77
|
});
|
|
74
78
|
|
|
@@ -29,19 +29,23 @@
|
|
|
29
29
|
<h4 class="hidden text-white md:flex">Actions</h4>
|
|
30
30
|
</div>
|
|
31
31
|
</Header>
|
|
32
|
-
<
|
|
33
|
-
v-if="
|
|
34
|
-
class="hidden-scrollbars
|
|
35
|
-
:style="{ maxHeight: dynamicTableHeight }"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
<RecycleScroller
|
|
33
|
+
v-if="props.accounts.length !== 0"
|
|
34
|
+
class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
|
|
35
|
+
:style="{ height: dynamicTableHeight, maxHeight: dynamicTableHeight }"
|
|
36
|
+
:items="virtualAccounts"
|
|
37
|
+
key-field="virtualKey"
|
|
38
|
+
:item-size="64"
|
|
39
|
+
@wheel.passive="handleVirtualWheel">
|
|
40
|
+
<template #default="{ item, index }">
|
|
41
|
+
<div class="min-h-16 flex-shrink-0 hover:bg-dark-550">
|
|
42
|
+
<Account
|
|
43
|
+
:class="getRowClass(index)"
|
|
44
|
+
:account="item.account"
|
|
45
|
+
:privacy="props.privacy" />
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
</RecycleScroller>
|
|
45
49
|
<EmptyState v-else :icon="MailIcon" message="No accounts found" subtitle="Create accounts to get started" />
|
|
46
50
|
</div>
|
|
47
51
|
</template>
|
|
@@ -59,21 +63,36 @@ import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
|
|
|
59
63
|
import EmptyState from "@/components/ui/EmptyState.vue";
|
|
60
64
|
import { useUIStore } from "@/stores/ui";
|
|
61
65
|
import { useDynamicTableHeight } from "@/composables/useDynamicTableHeight";
|
|
62
|
-
import { computed } from "vue";
|
|
63
|
-
import { useTableRender } from "@/composables/useTableRender";
|
|
64
66
|
import { getRowClass } from "@/utils/tableHelpers";
|
|
67
|
+
import { RecycleScroller } from "vue-virtual-scroller";
|
|
68
|
+
import { computed } from "vue";
|
|
65
69
|
|
|
66
70
|
const props = defineProps({
|
|
67
71
|
accounts: {
|
|
68
|
-
type:
|
|
72
|
+
type: Array,
|
|
69
73
|
required: true
|
|
74
|
+
},
|
|
75
|
+
privacy: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
default: true
|
|
70
78
|
}
|
|
71
79
|
});
|
|
72
80
|
const ui = useUIStore();
|
|
73
81
|
|
|
74
|
-
const { toRender } = useTableRender(computed(() => props.accounts));
|
|
75
|
-
|
|
76
82
|
import { TABLE_LAYOUT } from "@/constants/tableLayout";
|
|
77
83
|
|
|
78
84
|
const { dynamicTableHeight } = useDynamicTableHeight(TABLE_LAYOUT.ACCOUNTS);
|
|
85
|
+
|
|
86
|
+
const virtualAccounts = computed(() =>
|
|
87
|
+
props.accounts.map((account) => ({
|
|
88
|
+
account,
|
|
89
|
+
virtualKey: String(account.id ?? account.email ?? "account")
|
|
90
|
+
}))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const handleVirtualWheel = (event) => {
|
|
94
|
+
const target = event.currentTarget;
|
|
95
|
+
if (!target) return;
|
|
96
|
+
target.scrollTop += event.deltaY;
|
|
97
|
+
};
|
|
79
98
|
</script>
|
|
@@ -41,11 +41,21 @@
|
|
|
41
41
|
<FormField label="Password" :icon="KeyIcon" required :error="errors.includes('password')" z-index="0" class="col-span-12">
|
|
42
42
|
<input
|
|
43
43
|
placeholder="***********"
|
|
44
|
-
type="password"
|
|
44
|
+
:type="showPassword ? 'text' : 'password'"
|
|
45
45
|
v-model="account.password"
|
|
46
46
|
required
|
|
47
47
|
autocomplete="off"
|
|
48
48
|
name="account_password_disableautocomplete" />
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="ml-2 flex size-7 items-center justify-center rounded transition-colors hover:bg-dark-550"
|
|
52
|
+
@click="showPassword = !showPassword"
|
|
53
|
+
:aria-label="showPassword ? 'Hide password' : 'Show password'">
|
|
54
|
+
<img
|
|
55
|
+
:src="showPassword ? eyeOpenIcon : eyeClosedIcon"
|
|
56
|
+
:alt="showPassword ? 'Hide password' : 'Show password'"
|
|
57
|
+
class="h-4 w-4" />
|
|
58
|
+
</button>
|
|
49
59
|
</FormField>
|
|
50
60
|
</div>
|
|
51
61
|
|
|
@@ -71,17 +81,41 @@ import {
|
|
|
71
81
|
import { useUIStore } from "@/stores/ui";
|
|
72
82
|
import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
|
|
73
83
|
import ReadonlyFieldsSection from "@/components/ui/ReadonlyFieldsSection.vue";
|
|
74
|
-
import { ref } from "vue";
|
|
84
|
+
import { ref, toRaw } from "vue";
|
|
75
85
|
import { useFormValidation } from "@/composables/useFormValidation";
|
|
86
|
+
import eyeOpenIcon from "@/assets/img/eyes/open.svg";
|
|
87
|
+
import eyeClosedIcon from "@/assets/img/eyes/closed.svg";
|
|
76
88
|
|
|
77
89
|
const ui = useUIStore();
|
|
78
|
-
const
|
|
90
|
+
const createDefaultAccount = () => ({
|
|
79
91
|
email: "",
|
|
80
92
|
password: "",
|
|
81
93
|
tag: ui.profile.tags[0]
|
|
82
94
|
});
|
|
95
|
+
const cloneValue = (value) => {
|
|
96
|
+
const rawValue = toRaw(value);
|
|
97
|
+
if (typeof structuredClone === "function") {
|
|
98
|
+
try {
|
|
99
|
+
return structuredClone(rawValue);
|
|
100
|
+
} catch {
|
|
101
|
+
return JSON.parse(JSON.stringify(rawValue));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return JSON.parse(JSON.stringify(rawValue));
|
|
105
|
+
};
|
|
106
|
+
const getInitialAccount = () => {
|
|
107
|
+
if (!ui.currentlyEditing?.email) return createDefaultAccount();
|
|
83
108
|
|
|
84
|
-
|
|
109
|
+
const cloned = cloneValue(ui.currentlyEditing);
|
|
110
|
+
const fallbackTag = Array.isArray(cloned.tags) && cloned.tags.length ? cloned.tags[0] : ui.profile.tags[0];
|
|
111
|
+
return {
|
|
112
|
+
...createDefaultAccount(),
|
|
113
|
+
...cloned,
|
|
114
|
+
tag: cloned.tag || fallbackTag
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const account = ref(getInitialAccount());
|
|
118
|
+
const showPassword = ref(false);
|
|
85
119
|
|
|
86
120
|
const { errors, validateAccount } = useFormValidation();
|
|
87
121
|
|
|
@@ -160,7 +160,7 @@ import ReadonlyFieldsSection from "@/components/ui/ReadonlyFieldsSection.vue";
|
|
|
160
160
|
import ProfileCountryChooser from "@/components/Editors/Profile/ProfileCountryChooser.vue";
|
|
161
161
|
import { useUIStore } from "@/stores/ui";
|
|
162
162
|
import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
|
|
163
|
-
import { ref, watch, nextTick, onMounted } from "vue";
|
|
163
|
+
import { ref, watch, nextTick, onMounted, toRaw } from "vue";
|
|
164
164
|
import { fakeId } from "@/libs/utils/dataGeneration";
|
|
165
165
|
import { validateCard } from "@/libs/utils/validation";
|
|
166
166
|
import { useFormValidation } from "@/composables/useFormValidation";
|
|
@@ -228,17 +228,42 @@ const usStates = [
|
|
|
228
228
|
"WY"
|
|
229
229
|
];
|
|
230
230
|
const cardNumberInput = ref(null);
|
|
231
|
-
const
|
|
231
|
+
const createDefaultProfile = () => ({
|
|
232
232
|
cvv: "",
|
|
233
233
|
cardNumber: "",
|
|
234
234
|
city: "",
|
|
235
235
|
tag: ui.profile.tags[0],
|
|
236
236
|
state: "",
|
|
237
237
|
country: "US",
|
|
238
|
-
zipCode: ""
|
|
238
|
+
zipCode: "",
|
|
239
|
+
expMonth: "",
|
|
240
|
+
expYear: "",
|
|
241
|
+
address: ""
|
|
239
242
|
});
|
|
243
|
+
const cloneValue = (value) => {
|
|
244
|
+
const rawValue = toRaw(value);
|
|
245
|
+
if (typeof structuredClone === "function") {
|
|
246
|
+
try {
|
|
247
|
+
return structuredClone(rawValue);
|
|
248
|
+
} catch {
|
|
249
|
+
return JSON.parse(JSON.stringify(rawValue));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return JSON.parse(JSON.stringify(rawValue));
|
|
253
|
+
};
|
|
254
|
+
const getInitialProfile = () => {
|
|
255
|
+
if (!ui.currentlyEditing?.profileName) return createDefaultProfile();
|
|
240
256
|
|
|
241
|
-
|
|
257
|
+
const cloned = cloneValue(ui.currentlyEditing);
|
|
258
|
+
const fallbackTag = Array.isArray(cloned.tags) && cloned.tags.length ? cloned.tags[0] : ui.profile.tags[0];
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
...createDefaultProfile(),
|
|
262
|
+
...cloned,
|
|
263
|
+
tag: cloned.tag || fallbackTag
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
const formProfile = ref(getInitialProfile());
|
|
242
267
|
|
|
243
268
|
// Reactive display value for the formatted card number
|
|
244
269
|
const displayCardNumber = ref("");
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
@click="ui.setOpenContextMenu('')"
|
|
5
5
|
@click.right.prevent="ui.setOpenContextMenu('')"
|
|
6
6
|
@dblclick="handleDoubleClick"
|
|
7
|
-
@touchstart="handleTouchStart"
|
|
8
|
-
@touchend="handleTouchEnd">
|
|
7
|
+
@touchstart.passive="handleTouchStart"
|
|
8
|
+
@touchend.passive="handleTouchEnd">
|
|
9
9
|
<div class="col-span-2 sm:col-span-2 md:col-span-3 lg:col-span-2 flex">
|
|
10
10
|
<Checkbox
|
|
11
11
|
class="ml-0 mr-4"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<h4 class="flex items-center justify-center gap-2 text-center text-white">
|
|
20
20
|
<span class="hidden sm:block">
|
|
21
21
|
{{
|
|
22
|
-
props.
|
|
22
|
+
props.privacy
|
|
23
23
|
? props.profile.cardNumber[0] +
|
|
24
24
|
"•".repeat(props.profile.cardNumber.length - 5) +
|
|
25
25
|
props.profile.cardNumber.slice(-4)
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
<h4 class="text-center text-white">{{ expDate() }}</h4>
|
|
34
34
|
</div>
|
|
35
35
|
<div class="col-span-1 flex justify-center">
|
|
36
|
-
<StatusBadge :enabled="
|
|
36
|
+
<StatusBadge :enabled="isEnabled" size="small" />
|
|
37
37
|
</div>
|
|
38
38
|
|
|
39
39
|
<div class="col-span-1 hidden lg:block">
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<EditIcon />
|
|
50
50
|
</button>
|
|
51
51
|
</li>
|
|
52
|
-
<li v-if="
|
|
52
|
+
<li v-if="isEnabled">
|
|
53
53
|
<button @click="disable">
|
|
54
54
|
<img class="icon-md" src="/img/controls/disable.svg" />
|
|
55
55
|
</button>
|
|
@@ -85,6 +85,10 @@ const props = defineProps({
|
|
|
85
85
|
profile: {
|
|
86
86
|
type: Object,
|
|
87
87
|
required: true
|
|
88
|
+
},
|
|
89
|
+
privacy: {
|
|
90
|
+
type: Boolean,
|
|
91
|
+
default: true
|
|
88
92
|
}
|
|
89
93
|
});
|
|
90
94
|
|
|
@@ -98,7 +102,8 @@ const getAccountType = () => {
|
|
|
98
102
|
};
|
|
99
103
|
|
|
100
104
|
const expDate = () =>
|
|
101
|
-
props.
|
|
105
|
+
props.privacy ? "••/••" : `${props.profile.expMonth}/${props.profile.expYear?.replace("20", "")}`;
|
|
106
|
+
const isEnabled = computed(() => Boolean(props.profile.enabled));
|
|
102
107
|
const { enable, disable } = useEnableDisable(computed(() => props.profile), ui.addProfile);
|
|
103
108
|
const edit = () => {
|
|
104
109
|
ui.currentlyEditing = props.profile;
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
class="min-w-20 bg-dark-400 border border-dark-650 rounded-lg shadow-2xl z-50 p-2 [max-height:192px] overflow-y-auto custom-scrollbar-y"
|
|
13
13
|
:style="menuStyle"
|
|
14
14
|
@click.stop
|
|
15
|
-
@wheel.stop
|
|
16
|
-
@touchmove.stop>
|
|
15
|
+
@wheel.passive.stop
|
|
16
|
+
@touchmove.passive.stop>
|
|
17
17
|
<div
|
|
18
18
|
v-for="(country, i) in countries"
|
|
19
19
|
v-bind:key="country"
|