@knime/hub-features 1.2.24 → 1.3.0
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/CHANGELOG.md +18 -0
- package/package.json +6 -2
- package/src/components/Popover.vue +224 -0
- package/src/components/avatars/HubAccountAvatar.vue +30 -0
- package/src/components/avatars/HubAvatar.vue +46 -0
- package/src/components/avatars/HubGroupAvatar.vue +72 -0
- package/src/components/avatars/index.ts +1 -0
- package/src/components/versions/CreateVersionForm.vue +133 -0
- package/src/components/versions/CurrentState.vue +256 -0
- package/src/components/versions/LabelList.vue +254 -0
- package/src/components/versions/ManageVersions.vue +165 -0
- package/src/components/versions/NoVersionItem.vue +23 -0
- package/src/components/versions/VersionHistory.vue +137 -0
- package/src/components/versions/VersionItem.vue +277 -0
- package/src/components/versions/constants.ts +3 -0
- package/src/components/versions/index.ts +6 -0
- package/src/components/versions/types.ts +45 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
import { Button, LocalDateTime, SubMenu } from "@knime/components";
|
|
5
|
+
import MenuIcon from "@knime/styles/img/icons/menu-options.svg";
|
|
6
|
+
import WrenchIcon from "@knime/styles/img/icons/wrench.svg";
|
|
7
|
+
|
|
8
|
+
import HubAvatar from "../avatars/HubAvatar.vue";
|
|
9
|
+
|
|
10
|
+
import LabelList from "./LabelList.vue";
|
|
11
|
+
import type { ItemSavepoint, WithAvatar, WithLabels } from "./types";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
isSelected: boolean;
|
|
15
|
+
hasEditCapability: boolean;
|
|
16
|
+
currentStateSavepoint: ItemSavepoint & WithAvatar & WithLabels;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
createVersion: [];
|
|
21
|
+
discard: [];
|
|
22
|
+
select: [];
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const menuItems = computed(() => {
|
|
26
|
+
return props.hasEditCapability
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
text: "Discard",
|
|
30
|
+
action: () => emit("discard"),
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
: [];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const onMenuItemClick = (_: Event, item: (typeof menuItems.value)[number]) => {
|
|
37
|
+
item.action();
|
|
38
|
+
};
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="wrapper">
|
|
43
|
+
<div
|
|
44
|
+
class="current-state"
|
|
45
|
+
:class="{ selected: isSelected }"
|
|
46
|
+
@click="!isSelected && $emit('select')"
|
|
47
|
+
>
|
|
48
|
+
<div class="left">
|
|
49
|
+
<div class="draft">
|
|
50
|
+
<h6><WrenchIcon class="draft-icon" /><span>Draft</span></h6>
|
|
51
|
+
|
|
52
|
+
<div class="labels">
|
|
53
|
+
<LabelList :labels="currentStateSavepoint.labels" />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<span class="date">
|
|
57
|
+
Latest edits on
|
|
58
|
+
<LocalDateTime
|
|
59
|
+
class="date"
|
|
60
|
+
show-time
|
|
61
|
+
:date="currentStateSavepoint.changes[0].createdOn"
|
|
62
|
+
/>
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="right">
|
|
67
|
+
<Button
|
|
68
|
+
v-if="hasEditCapability"
|
|
69
|
+
with-border
|
|
70
|
+
compact
|
|
71
|
+
class="create-button"
|
|
72
|
+
@click="$emit('createVersion')"
|
|
73
|
+
>
|
|
74
|
+
Create version
|
|
75
|
+
</Button>
|
|
76
|
+
<HubAvatar
|
|
77
|
+
class="profile-picture"
|
|
78
|
+
v-bind="currentStateSavepoint.avatar"
|
|
79
|
+
/>
|
|
80
|
+
<div v-if="menuItems.length" class="controls">
|
|
81
|
+
<nav>
|
|
82
|
+
<SubMenu
|
|
83
|
+
:items="menuItems"
|
|
84
|
+
button-title="Options"
|
|
85
|
+
@item-click="onMenuItemClick"
|
|
86
|
+
>
|
|
87
|
+
<MenuIcon class="open-icon" />
|
|
88
|
+
</SubMenu>
|
|
89
|
+
</nav>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style lang="postcss" scoped>
|
|
97
|
+
@import url("@knime/styles/css/mixins.css");
|
|
98
|
+
|
|
99
|
+
.wrapper {
|
|
100
|
+
position: relative;
|
|
101
|
+
margin-left: 30px;
|
|
102
|
+
|
|
103
|
+
&::before {
|
|
104
|
+
content: "";
|
|
105
|
+
position: absolute;
|
|
106
|
+
left: -22px;
|
|
107
|
+
top: 0;
|
|
108
|
+
width: 2px;
|
|
109
|
+
height: calc(100% + 30px);
|
|
110
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 5 5'%3E%3Ccircle cx='1' cy='1' r='.5' fill='%236E6E6E'/%3E%3C/svg%3E");
|
|
111
|
+
background-size: 5px;
|
|
112
|
+
background-repeat: repeat-y;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
& .current-state {
|
|
116
|
+
background-color: var(--knime-gray-ultra-light);
|
|
117
|
+
display: flex;
|
|
118
|
+
justify-content: space-between;
|
|
119
|
+
align-items: center;
|
|
120
|
+
padding: 10px;
|
|
121
|
+
gap: 10px;
|
|
122
|
+
transition: background-color 0.25s ease;
|
|
123
|
+
|
|
124
|
+
& .left {
|
|
125
|
+
& .draft {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
gap: 3px;
|
|
129
|
+
overflow-wrap: anywhere;
|
|
130
|
+
word-break: break-word;
|
|
131
|
+
|
|
132
|
+
& h6 {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 13px;
|
|
137
|
+
line-height: 18px;
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
transition: color 0.25s ease;
|
|
140
|
+
|
|
141
|
+
& .draft-icon {
|
|
142
|
+
@mixin svg-icon-size 20;
|
|
143
|
+
|
|
144
|
+
margin-right: 4px;
|
|
145
|
+
background: var(--knime-carrot-light);
|
|
146
|
+
padding: 4px;
|
|
147
|
+
border-radius: 1000px;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
& .labels {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-flow: row wrap;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 5px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
& .date {
|
|
159
|
+
margin: 0;
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
line-height: 16px;
|
|
162
|
+
font-weight: 300;
|
|
163
|
+
transition: color 0.25s ease;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
& .right {
|
|
169
|
+
display: flex;
|
|
170
|
+
justify-content: flex-end;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 8px;
|
|
173
|
+
|
|
174
|
+
& .create-button {
|
|
175
|
+
white-space: nowrap;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
& .controls {
|
|
179
|
+
display: flex;
|
|
180
|
+
justify-content: center;
|
|
181
|
+
flex-direction: column;
|
|
182
|
+
border-left: 1px solid var(--knime-silver-sand);
|
|
183
|
+
padding-left: 5px;
|
|
184
|
+
margin-right: -4px;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
&::before {
|
|
189
|
+
content: "";
|
|
190
|
+
width: 12px;
|
|
191
|
+
height: 12px;
|
|
192
|
+
background-color: var(--knime-gray-ultra-light);
|
|
193
|
+
transform: rotateZ(45deg);
|
|
194
|
+
position: absolute;
|
|
195
|
+
left: -6px;
|
|
196
|
+
top: 12px;
|
|
197
|
+
transition: background-color 0.25s ease;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&::after {
|
|
201
|
+
content: "";
|
|
202
|
+
position: absolute;
|
|
203
|
+
left: -24px;
|
|
204
|
+
top: 15px;
|
|
205
|
+
background-color: var(--knime-masala);
|
|
206
|
+
width: 6px;
|
|
207
|
+
height: 6px;
|
|
208
|
+
border-radius: 100%;
|
|
209
|
+
box-shadow: 0 0 0 0 transparent;
|
|
210
|
+
transition:
|
|
211
|
+
background-color 0.25s ease,
|
|
212
|
+
box-shadow 0.25s ease;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
&:hover {
|
|
216
|
+
box-shadow: 0 2px 10px 0 var(--knime-gray-dark-semi);
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&.selected {
|
|
221
|
+
background-color: var(--knime-cornflower-semi);
|
|
222
|
+
|
|
223
|
+
& .left {
|
|
224
|
+
& h6,
|
|
225
|
+
& .date {
|
|
226
|
+
color: var(--knime-cornflower-dark);
|
|
227
|
+
|
|
228
|
+
& .draft-icon {
|
|
229
|
+
background: none;
|
|
230
|
+
stroke: var(--knime-cornflower-dark);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
& .controls {
|
|
236
|
+
border-left: 1px solid var(--knime-cornflower-light);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
&::before {
|
|
240
|
+
background-color: var(--knime-cornflower-semi);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
&::after {
|
|
244
|
+
background-color: var(--knime-cornflower);
|
|
245
|
+
box-shadow:
|
|
246
|
+
0 0 0 3px var(--knime-porcelain),
|
|
247
|
+
0 0 0 5px var(--knime-cornflower);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@media only screen and (width <= 900px) {
|
|
252
|
+
width: calc(100vw - 100px);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
</style>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type Ref, computed, onMounted, ref } from "vue";
|
|
3
|
+
import { useEventBus } from "@vueuse/core";
|
|
4
|
+
import { autoUpdate, offset, useFloating } from "@floating-ui/vue";
|
|
5
|
+
import { isEqual } from "lodash-es";
|
|
6
|
+
|
|
7
|
+
import { FunctionButton } from "@knime/components";
|
|
8
|
+
import { truncateString } from "@knime/utils";
|
|
9
|
+
|
|
10
|
+
import Popover from "../Popover.vue";
|
|
11
|
+
|
|
12
|
+
import type { AssignedLabel } from "./types";
|
|
13
|
+
|
|
14
|
+
interface PopoverElement extends HTMLElement {
|
|
15
|
+
closeMenu: () => void;
|
|
16
|
+
openMenu: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LABEL_LENGTH = 25;
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(
|
|
22
|
+
defineProps<{
|
|
23
|
+
labels: Array<AssignedLabel>;
|
|
24
|
+
defaultLabelCount?: number;
|
|
25
|
+
}>(),
|
|
26
|
+
{ defaultLabelCount: 3 },
|
|
27
|
+
);
|
|
28
|
+
const emit = defineEmits(["labelOver", "labelLeave"]);
|
|
29
|
+
|
|
30
|
+
const eventBus = useEventBus("versionLabels");
|
|
31
|
+
const eventBusKey = "versionLabelShowPopover";
|
|
32
|
+
|
|
33
|
+
const showAll = ref(false);
|
|
34
|
+
const activeLabel: Ref<AssignedLabel | null> = ref(null);
|
|
35
|
+
|
|
36
|
+
const popover: Ref<PopoverElement | null> = ref(null);
|
|
37
|
+
const floatingPanel: Ref<HTMLElement | null> = ref(null);
|
|
38
|
+
const floatingAnchor: Ref<HTMLElement | null> = ref(null);
|
|
39
|
+
// 40px is the offset of the arrow on the popover plus the top padding
|
|
40
|
+
// this is necessary to compensate for the offset calculation
|
|
41
|
+
const defaultCrossAxisOffset = -46;
|
|
42
|
+
const defaultMainAxisOffset = 75;
|
|
43
|
+
|
|
44
|
+
const createMiddleware = () => [
|
|
45
|
+
offset({
|
|
46
|
+
crossAxis: defaultCrossAxisOffset,
|
|
47
|
+
mainAxis: defaultMainAxisOffset,
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const { floatingStyles } = useFloating(floatingAnchor, floatingPanel, {
|
|
52
|
+
whileElementsMounted: autoUpdate,
|
|
53
|
+
placement: "left-start",
|
|
54
|
+
strategy: "fixed",
|
|
55
|
+
middleware: createMiddleware(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const popoverTopOffset = ref(0);
|
|
59
|
+
const popoverFloatingStyles = computed(() => {
|
|
60
|
+
const parsedTop = parseInt(floatingStyles.value.top.replace("px", ""), 10);
|
|
61
|
+
const compensatedTop = `${parsedTop + popoverTopOffset.value}px`;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...floatingStyles.value,
|
|
65
|
+
top: compensatedTop,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const isShowMoreVisible = computed(
|
|
70
|
+
() => props.labels.length > props.defaultLabelCount && !showAll.value,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const showMoreButtonText = computed(
|
|
74
|
+
() => `+${props.labels.length - props.defaultLabelCount}`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const filteredLabels = computed(() =>
|
|
78
|
+
showAll.value ? props.labels : props.labels.slice(0, props.defaultLabelCount),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const calculateDistanceToUpperBorder = (labelElement: HTMLElement) => {
|
|
82
|
+
const labelRect = labelElement.getBoundingClientRect();
|
|
83
|
+
const floatingAnchorRect = floatingAnchor.value!.getBoundingClientRect();
|
|
84
|
+
|
|
85
|
+
return Math.round(labelRect.top - floatingAnchorRect.top);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const isActiveLabel = (label: AssignedLabel) =>
|
|
89
|
+
isEqual(activeLabel.value, label);
|
|
90
|
+
|
|
91
|
+
const togglePopover = (label: AssignedLabel, labelElement: HTMLElement) => {
|
|
92
|
+
if (isActiveLabel(label)) {
|
|
93
|
+
activeLabel.value = null;
|
|
94
|
+
popover.value?.closeMenu();
|
|
95
|
+
} else {
|
|
96
|
+
eventBus.emit(eventBusKey);
|
|
97
|
+
popoverTopOffset.value = calculateDistanceToUpperBorder(labelElement);
|
|
98
|
+
|
|
99
|
+
activeLabel.value = label;
|
|
100
|
+
popover.value?.openMenu();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const showMore = () => {
|
|
105
|
+
showAll.value = !showAll.value;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const mouseOver = () => emit("labelOver");
|
|
109
|
+
const mouseLeave = () => emit("labelLeave");
|
|
110
|
+
|
|
111
|
+
onMounted(() => {
|
|
112
|
+
eventBus.on((event: unknown) => {
|
|
113
|
+
if (event === eventBusKey) {
|
|
114
|
+
popover.value?.closeMenu();
|
|
115
|
+
activeLabel.value = null;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
</script>
|
|
120
|
+
|
|
121
|
+
<template>
|
|
122
|
+
<template v-if="labels.length > 0">
|
|
123
|
+
<div class="label-list-with-popover">
|
|
124
|
+
<div ref="floatingAnchor" class="label-list">
|
|
125
|
+
<FunctionButton
|
|
126
|
+
v-for="label in filteredLabels"
|
|
127
|
+
:key="label.labelId"
|
|
128
|
+
compact
|
|
129
|
+
class="with-border"
|
|
130
|
+
:active="isActiveLabel(label)"
|
|
131
|
+
@mouseover="mouseOver"
|
|
132
|
+
@mouseleave="mouseLeave"
|
|
133
|
+
@click.stop="togglePopover(label, $event.currentTarget)"
|
|
134
|
+
@keydown.esc.stop="popover?.closeMenu()"
|
|
135
|
+
>
|
|
136
|
+
{{ truncateString(label.label.name, LABEL_LENGTH) }}
|
|
137
|
+
</FunctionButton>
|
|
138
|
+
|
|
139
|
+
<FunctionButton
|
|
140
|
+
v-if="isShowMoreVisible"
|
|
141
|
+
compact
|
|
142
|
+
class="with-border"
|
|
143
|
+
@mouseover="mouseOver"
|
|
144
|
+
@mouseleave="mouseLeave"
|
|
145
|
+
@click.stop="showMore"
|
|
146
|
+
>{{ showMoreButtonText }}</FunctionButton
|
|
147
|
+
>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div
|
|
151
|
+
ref="floatingPanel"
|
|
152
|
+
class="floating-panel"
|
|
153
|
+
:style="popoverFloatingStyles"
|
|
154
|
+
@click.stop
|
|
155
|
+
@mouseover="mouseOver"
|
|
156
|
+
@mouseleave="mouseLeave"
|
|
157
|
+
>
|
|
158
|
+
<Popover
|
|
159
|
+
ref="popover"
|
|
160
|
+
:use-button="false"
|
|
161
|
+
:button-with-border="false"
|
|
162
|
+
arrow-position="right"
|
|
163
|
+
@close="activeLabel = null"
|
|
164
|
+
>
|
|
165
|
+
<template #content>
|
|
166
|
+
<h6 class="headline">
|
|
167
|
+
{{ activeLabel?.label.name }}
|
|
168
|
+
</h6>
|
|
169
|
+
<div class="panel">
|
|
170
|
+
<div class="description">
|
|
171
|
+
{{ activeLabel?.label.description }}
|
|
172
|
+
</div>
|
|
173
|
+
<div class="message">{{ activeLabel?.message }}</div>
|
|
174
|
+
</div>
|
|
175
|
+
</template>
|
|
176
|
+
</Popover>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
180
|
+
</template>
|
|
181
|
+
|
|
182
|
+
<style lang="postcss" scoped>
|
|
183
|
+
.label-list-with-popover {
|
|
184
|
+
position: relative;
|
|
185
|
+
|
|
186
|
+
& .label-list {
|
|
187
|
+
position: relative;
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-flow: row wrap;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: var(--space-4);
|
|
192
|
+
padding: var(--space-6) 0;
|
|
193
|
+
|
|
194
|
+
& .function-button {
|
|
195
|
+
&.with-border {
|
|
196
|
+
padding: 2px var(--space-8);
|
|
197
|
+
border: 1px solid var(--knime-silver-sand);
|
|
198
|
+
|
|
199
|
+
&.active {
|
|
200
|
+
border-color: var(--theme-button-function-background-color-active);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
&:empty {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
& .floating-panel {
|
|
211
|
+
z-index: 3;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* the position is set back because it interferes with the position calculation of floating UI */
|
|
216
|
+
.popover.expanded :deep(.content) {
|
|
217
|
+
position: initial;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
& .headline,
|
|
221
|
+
& .panel {
|
|
222
|
+
line-height: 1.5;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
& .headline {
|
|
226
|
+
font-size: 16px;
|
|
227
|
+
font-weight: 700;
|
|
228
|
+
margin: 0;
|
|
229
|
+
margin: 0 0 var(--space-8);
|
|
230
|
+
padding-right: 18px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
& .panel {
|
|
234
|
+
& .description {
|
|
235
|
+
margin-bottom: var(--space-6);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
& .message,
|
|
239
|
+
& .description {
|
|
240
|
+
font-weight: 300;
|
|
241
|
+
font-size: 13px;
|
|
242
|
+
|
|
243
|
+
&:empty {
|
|
244
|
+
display: none;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@media only screen and (width <= 900px) {
|
|
250
|
+
.label-list-with-popover {
|
|
251
|
+
display: none;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useEventBus } from "@vueuse/core";
|
|
3
|
+
import { throttle } from "lodash-es";
|
|
4
|
+
|
|
5
|
+
import { FunctionButton } from "@knime/components";
|
|
6
|
+
import CloseIcon from "@knime/styles/img/icons/close.svg";
|
|
7
|
+
import HistoryIcon from "@knime/styles/img/icons/history.svg";
|
|
8
|
+
|
|
9
|
+
import CurrentState from "./CurrentState.vue";
|
|
10
|
+
import VersionHistory from "./VersionHistory.vue";
|
|
11
|
+
import { CURRENT_STATE_VERSION } from "./constants";
|
|
12
|
+
import type {
|
|
13
|
+
ItemSavepoint,
|
|
14
|
+
NamedItemVersion,
|
|
15
|
+
WithAvatar,
|
|
16
|
+
WithLabels,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
type ManageVersionsProps = {
|
|
20
|
+
hasUnversionedChanges: boolean;
|
|
21
|
+
unversionedSavepoint?: ItemSavepoint & WithAvatar & WithLabels;
|
|
22
|
+
currentVersion: NamedItemVersion["version"] | null;
|
|
23
|
+
versionHistory: Array<NamedItemVersion & WithAvatar & WithLabels>;
|
|
24
|
+
loading: boolean;
|
|
25
|
+
hasLoadedAllVersions: boolean;
|
|
26
|
+
hasAdminRights: boolean;
|
|
27
|
+
hasEditCapability: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
defineProps<ManageVersionsProps>();
|
|
31
|
+
|
|
32
|
+
defineEmits<{
|
|
33
|
+
close: [];
|
|
34
|
+
loadAll: [];
|
|
35
|
+
create: [];
|
|
36
|
+
delete: [version: NamedItemVersion["version"]];
|
|
37
|
+
restore: [version: NamedItemVersion["version"]];
|
|
38
|
+
select: [version: NamedItemVersion["version"]];
|
|
39
|
+
}>();
|
|
40
|
+
|
|
41
|
+
const labelsEventBus = useEventBus("versionLabels");
|
|
42
|
+
|
|
43
|
+
const closeLabelPopovers = throttle(() => {
|
|
44
|
+
labelsEventBus.emit("versionLabelShowPopover");
|
|
45
|
+
// eslint-disable-next-line no-magic-numbers
|
|
46
|
+
}, 10000); // Arbitrary delay to reduce overhead, is automatically reset @scrollend
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div class="manage-versions-container">
|
|
51
|
+
<FunctionButton class="close" @click="$emit('close')">
|
|
52
|
+
<CloseIcon />
|
|
53
|
+
</FunctionButton>
|
|
54
|
+
|
|
55
|
+
<div class="manage-versions">
|
|
56
|
+
<div class="header">
|
|
57
|
+
<HistoryIcon class="history-icon" /><!--
|
|
58
|
+
-->
|
|
59
|
+
<h4>Version history</h4>
|
|
60
|
+
<div
|
|
61
|
+
v-if="hasUnversionedChanges && unversionedSavepoint"
|
|
62
|
+
class="changes"
|
|
63
|
+
>
|
|
64
|
+
<CurrentState
|
|
65
|
+
:has-edit-capability
|
|
66
|
+
:is-selected="currentVersion === CURRENT_STATE_VERSION"
|
|
67
|
+
:current-state-savepoint="unversionedSavepoint"
|
|
68
|
+
@select="$emit('select', CURRENT_STATE_VERSION)"
|
|
69
|
+
@create-version="$emit('create')"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div
|
|
74
|
+
class="overflow-container"
|
|
75
|
+
@scroll="closeLabelPopovers"
|
|
76
|
+
@scrollend="closeLabelPopovers.cancel"
|
|
77
|
+
>
|
|
78
|
+
<div class="versions">
|
|
79
|
+
<VersionHistory
|
|
80
|
+
:selected-version="currentVersion"
|
|
81
|
+
:version-history
|
|
82
|
+
:loading
|
|
83
|
+
:has-unversioned-changes
|
|
84
|
+
:has-loaded-all-versions
|
|
85
|
+
:has-admin-rights
|
|
86
|
+
:has-edit-capability
|
|
87
|
+
@delete="$emit('delete', $event)"
|
|
88
|
+
@restore="$emit('restore', $event)"
|
|
89
|
+
@load-all="$emit('loadAll')"
|
|
90
|
+
@select="$emit('select', $event ?? CURRENT_STATE_VERSION)"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<style lang="postcss" scoped>
|
|
99
|
+
@import url("@knime/styles/css/mixins.css");
|
|
100
|
+
|
|
101
|
+
.manage-versions-container {
|
|
102
|
+
position: relative;
|
|
103
|
+
background-color: var(--knime-gray-light-semi);
|
|
104
|
+
height: 100%;
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
justify-content: flex-start;
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
overscroll-behavior: none;
|
|
110
|
+
isolation: isolate;
|
|
111
|
+
|
|
112
|
+
& .close {
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 6px;
|
|
115
|
+
right: 6px;
|
|
116
|
+
z-index: 3;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
& .manage-versions {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
width: 100%;
|
|
123
|
+
height: 100%;
|
|
124
|
+
|
|
125
|
+
& .header {
|
|
126
|
+
background-color: var(--knime-white);
|
|
127
|
+
z-index: 2;
|
|
128
|
+
box-shadow: var(--shadow-elevation-1);
|
|
129
|
+
padding: 32px 30px 30px;
|
|
130
|
+
|
|
131
|
+
& h4 {
|
|
132
|
+
display: inline;
|
|
133
|
+
margin: 0;
|
|
134
|
+
font-size: 22px;
|
|
135
|
+
line-height: 26px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
& .history-icon {
|
|
139
|
+
@mixin svg-icon-size 24;
|
|
140
|
+
|
|
141
|
+
margin-right: 9px;
|
|
142
|
+
vertical-align: sub;
|
|
143
|
+
stroke: var(--knime-masala);
|
|
144
|
+
position: relative;
|
|
145
|
+
top: 1px;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
& .overflow-container {
|
|
150
|
+
overflow-y: auto;
|
|
151
|
+
|
|
152
|
+
& .versions {
|
|
153
|
+
width: 100%;
|
|
154
|
+
padding: 30px;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
& .changes {
|
|
159
|
+
background-color: var(--knime-white);
|
|
160
|
+
width: 100%;
|
|
161
|
+
padding-top: 30px;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="no-versions-item">No version created yet</div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<style lang="postcss" scoped>
|
|
6
|
+
.no-versions-item {
|
|
7
|
+
font-weight: 300;
|
|
8
|
+
font-size: 12px;
|
|
9
|
+
line-height: 15px;
|
|
10
|
+
padding-top: 4px;
|
|
11
|
+
|
|
12
|
+
&::after {
|
|
13
|
+
content: "";
|
|
14
|
+
position: absolute;
|
|
15
|
+
left: -4px;
|
|
16
|
+
top: 11px;
|
|
17
|
+
background-color: var(--knime-masala);
|
|
18
|
+
width: 6px;
|
|
19
|
+
height: 6px;
|
|
20
|
+
border-radius: 100%;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</style>
|