@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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @knime/hub-features
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e9beadc: Port VersionManager components
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [6c99c22]
12
+ - @knime/components@1.20.0
13
+
14
+ ## 1.2.25
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [5f3ca36]
19
+ - @knime/components@1.19.4
20
+
3
21
  ## 1.2.24
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knime/hub-features",
3
- "version": "1.2.24",
3
+ "version": "1.3.0",
4
4
  "description": "Vue components & composables for shared hub features",
5
5
  "homepage": "https://knime.github.io/webapps-common/",
6
6
  "license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
@@ -18,16 +18,20 @@
18
18
  "exports": {
19
19
  ".": {
20
20
  "import": "./src/index.ts"
21
+ },
22
+ "./versions": {
23
+ "import": "./src/components/versions/index.ts"
21
24
  }
22
25
  },
23
26
  "dependencies": {
27
+ "@floating-ui/vue": "1.1.5",
24
28
  "@vueuse/components": "~10.11.1",
25
29
  "@vueuse/core": "~10.11.1",
26
30
  "@vueuse/shared": "~10.11.1",
27
31
  "lodash-es": "4.17.21",
28
32
  "ofetch": "^1.4.1",
29
33
  "typescript": "^5.4.5",
30
- "@knime/components": "1.19.3",
34
+ "@knime/components": "1.20.0",
31
35
  "@knime/styles": "1.3.1",
32
36
  "@knime/utils": "1.3.1"
33
37
  },
@@ -0,0 +1,224 @@
1
+ <script setup lang="ts">
2
+ import { type Ref, ref } from "vue";
3
+ import { onClickOutside } from "@vueuse/core";
4
+
5
+ import { FunctionButton } from "@knime/components";
6
+ import CloseIcon from "@knime/styles/img/icons/close.svg";
7
+
8
+ type Props = {
9
+ disabled?: boolean;
10
+ buttonTitle?: string | null;
11
+ useButton?: boolean;
12
+ buttonWithBorder?: boolean;
13
+ hasCloseButton?: boolean;
14
+ arrowPosition?: "top" | "right";
15
+ };
16
+
17
+ withDefaults(defineProps<Props>(), {
18
+ buttonTitle: null,
19
+ arrowPosition: "top",
20
+ useButton: true,
21
+ bottonWithBorder: true,
22
+ hasCloseButton: true,
23
+ });
24
+
25
+ const emit = defineEmits(["open", "close"]);
26
+
27
+ const expanded = ref(false);
28
+ const toggleRef: Ref<InstanceType<typeof FunctionButton> | null> = ref(null);
29
+ const expandedElemRef = ref<HTMLElement | null>(null);
30
+
31
+ const closeMenu = () => {
32
+ if (expanded.value) {
33
+ consola.trace("Closing popover menu");
34
+ emit("close");
35
+ expanded.value = false;
36
+ }
37
+ };
38
+ const openMenu = () => {
39
+ consola.trace("Opening popover menu");
40
+ emit("open");
41
+ expanded.value = true;
42
+ };
43
+ const onToggle = () => {
44
+ if (expanded.value) {
45
+ closeMenu();
46
+ } else {
47
+ openMenu();
48
+ }
49
+ };
50
+ const onFocus = () => {
51
+ toggleRef.value?.focus();
52
+ };
53
+
54
+ onClickOutside(expandedElemRef, closeMenu, { capture: false });
55
+
56
+ defineExpose({
57
+ openMenu,
58
+ closeMenu,
59
+ onToggle,
60
+ });
61
+ </script>
62
+
63
+ <template>
64
+ <div
65
+ ref="wrapper"
66
+ :class="['popover', `arrow-${arrowPosition}`, { expanded }]"
67
+ :expanded="expanded"
68
+ @focus="onFocus"
69
+ >
70
+ <!-- use mouseup instead of click as the click event fires twice on key input in Firefox-->
71
+ <FunctionButton
72
+ v-if="useButton"
73
+ ref="toggleRef"
74
+ :title="buttonTitle"
75
+ class="toggle"
76
+ :class="{ 'with-border': buttonWithBorder }"
77
+ :active="expanded"
78
+ :disabled="disabled"
79
+ @mouseup.stop="onToggle"
80
+ @keydown.space.stop="onToggle"
81
+ @keydown.esc.stop="closeMenu"
82
+ >
83
+ <slot name="button" />
84
+ </FunctionButton>
85
+ <span
86
+ v-else
87
+ ref="toggleRef"
88
+ tabindex="0"
89
+ :title="buttonTitle ?? undefined"
90
+ :active="expanded"
91
+ :class="['toggle', { disabled, 'with-border': buttonWithBorder }]"
92
+ @mouseup.stop="onToggle"
93
+ @keydown.space.prevent="onToggle"
94
+ @keydown.esc.prevent="closeMenu"
95
+ >
96
+ <slot name="button" />
97
+ </span>
98
+ <div v-if="expanded" ref="expandedElemRef" class="content">
99
+ <slot name="content" />
100
+ <FunctionButton
101
+ v-if="hasCloseButton"
102
+ class="closer"
103
+ @click.stop="closeMenu"
104
+ >
105
+ <CloseIcon />
106
+ </FunctionButton>
107
+ </div>
108
+ </div>
109
+ </template>
110
+
111
+ <style lang="postcss" scoped>
112
+ .popover {
113
+ position: relative;
114
+ --popover-top-margin: 18px;
115
+ --popover-arrow-size: 15px;
116
+
117
+ filter: drop-shadow(0 2px 10px var(--knime-gray-dark-semi));
118
+
119
+ &.expanded {
120
+ z-index: 1;
121
+
122
+ &::after {
123
+ position: absolute;
124
+ content: "";
125
+ width: 0;
126
+ height: 0;
127
+ }
128
+
129
+ /* transparent background to maintain focus on the popover while transitioning into it with the mouse */
130
+ &::before {
131
+ content: "";
132
+ position: absolute;
133
+ background-color: rgb(0 0 0 / 0%);
134
+ }
135
+
136
+ &.arrow-top {
137
+ &::after {
138
+ top: calc(100% + var(--popover-top-margin) - var(--popover-arrow-size));
139
+ right: 20px;
140
+ border-left: var(--popover-arrow-size) solid transparent;
141
+ border-right: var(--popover-arrow-size) solid transparent;
142
+ border-bottom: var(--popover-arrow-size) solid var(--knime-white);
143
+
144
+ @media only screen and (width <= 900px) {
145
+ border-top: var(--popover-arrow-size) solid var(--knime-white);
146
+ border-bottom: 0;
147
+ bottom: calc(
148
+ 100% + var(--popover-top-margin) - var(--popover-arrow-size)
149
+ );
150
+ top: auto;
151
+ }
152
+ }
153
+
154
+ &::before {
155
+ top: calc(100% + var(--popover-top-margin) - var(--popover-arrow-size));
156
+ right: 0;
157
+ width: var(--popover-width, 300px);
158
+ height: var(--popover-arrow-size);
159
+ }
160
+ }
161
+
162
+ &.arrow-right {
163
+ &::after {
164
+ top: 40px;
165
+ left: var(--popover-width, 300px);
166
+ width: var(--popover-arrow-size);
167
+ height: auto;
168
+ aspect-ratio: 1/2;
169
+ background: var(--knime-white);
170
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
171
+ }
172
+ }
173
+ }
174
+
175
+ & .content {
176
+ width: var(--popover-width, 300px);
177
+ position: absolute;
178
+ top: calc(
179
+ 100% + var(--popover-top-margin) - 1px
180
+ ); /* shift by one pixel to avoid zoom issues */
181
+
182
+ right: 0;
183
+ padding: 20px;
184
+ background: var(--knime-white);
185
+
186
+ @media only screen and (width <= 900px) {
187
+ bottom: calc(
188
+ 100% + var(--popover-top-margin) - 1px
189
+ ); /* shift by one pixel to avoid zoom issues */
190
+
191
+ top: auto;
192
+ }
193
+ }
194
+
195
+ & .toggle {
196
+ display: flex;
197
+
198
+ &:focus {
199
+ outline: none;
200
+ }
201
+
202
+ &.disabled {
203
+ opacity: 0.5;
204
+ pointer-events: none;
205
+ }
206
+ }
207
+
208
+ & .with-border {
209
+ &.function-button {
210
+ border: 1px solid var(--knime-silver-sand);
211
+
212
+ &.active {
213
+ border-color: var(--theme-button-function-background-color-active);
214
+ }
215
+ }
216
+ }
217
+
218
+ & .closer {
219
+ position: absolute;
220
+ top: 6px;
221
+ right: 6px;
222
+ }
223
+ }
224
+ </style>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { Avatar } from "@knime/components";
3
+
4
+ import type { HubAccountAvatarData } from "./HubAvatar.vue";
5
+ /**
6
+ * Displays the avatar image for an account (i.e., team or user)
7
+ * or a colored placeholder. See HubAvatar.
8
+ */
9
+ withDefaults(defineProps<HubAccountAvatarData>(), {
10
+ isTrusted: false,
11
+ });
12
+ </script>
13
+
14
+ <template>
15
+ <Avatar
16
+ :image
17
+ :name
18
+ :tooltip
19
+ :show-text
20
+ :layout
21
+ :show-trusted="isTrusted"
22
+ :link-url
23
+ >
24
+ <template #overlay>
25
+ <slot />
26
+ </template>
27
+ </Avatar>
28
+ </template>
29
+
30
+ <style lang="postcss" scoped></style>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ import HubAccountAvatar from "./HubAccountAvatar.vue";
5
+ import HubGroupAvatar from "./HubGroupAvatar.vue";
6
+
7
+ /**
8
+ * Avatar component which displays an image or a colored placeholder as well as a text
9
+ * for accounts (i.e., teams or users) or for groups. Depending on the entity for which
10
+ * an avatar image should be shown, this component renders a HubGroupAvatar or HubAccountAvatar.
11
+ */
12
+
13
+ type CommonAvatarData = {
14
+ name: string;
15
+ linkUrl?: string;
16
+ layout?: "vertical" | "horizontal";
17
+ showText?: "name" | "placeholder" | "none";
18
+ };
19
+
20
+ export type HubGroupAvatarData = CommonAvatarData & {
21
+ kind: "group";
22
+ membersCount?: number;
23
+ isAdminGroup?: boolean;
24
+ };
25
+
26
+ export type HubAccountAvatarData = CommonAvatarData & {
27
+ kind: "account";
28
+ image?: { url?: string | null; altText?: string };
29
+ isTrusted?: boolean;
30
+ tooltip?: string;
31
+ };
32
+
33
+ export type HubAvatarData = HubGroupAvatarData | HubAccountAvatarData;
34
+
35
+ const props = defineProps<HubAvatarData>();
36
+
37
+ const renderComponent = computed(() => {
38
+ return props.kind === "group" ? HubGroupAvatar : HubAccountAvatar;
39
+ });
40
+ </script>
41
+
42
+ <template>
43
+ <Component :is="renderComponent" v-bind="props">
44
+ <slot />
45
+ </Component>
46
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+
4
+ import { Avatar } from "@knime/components";
5
+ import AdminGroupIcon from "@knime/styles/img/icons/team-group-admin.svg";
6
+ import MemberGroupIcon from "@knime/styles/img/icons/team-group.svg";
7
+
8
+ import type { HubGroupAvatarData } from "./HubAvatar.vue";
9
+
10
+ /**
11
+ * Displays the avatar image for a group.
12
+ * See HubAvatar for more details.
13
+ */
14
+ const props = defineProps<HubGroupAvatarData>();
15
+
16
+ const membersCountPostfix = computed(() => {
17
+ if (props.membersCount === 0) {
18
+ return " (0 people)";
19
+ }
20
+ if (props.membersCount === 1) {
21
+ return " (1 person)";
22
+ }
23
+
24
+ return ` (${props.membersCount} people)`;
25
+ });
26
+
27
+ const displayText = computed(() => {
28
+ return `${props.name}${membersCountPostfix.value}`;
29
+ });
30
+ </script>
31
+
32
+ <template>
33
+ <Avatar
34
+ :name="displayText"
35
+ :tooltip="displayText"
36
+ :show-text
37
+ :layout
38
+ :link-url
39
+ descriptor="Group"
40
+ >
41
+ <template #default>
42
+ <div class="container">
43
+ <AdminGroupIcon v-if="isAdminGroup" class="admin" />
44
+ <MemberGroupIcon v-else class="member" />
45
+ </div>
46
+ </template>
47
+
48
+ <template #overlay>
49
+ <slot />
50
+ </template>
51
+ </Avatar>
52
+ </template>
53
+
54
+ <style lang="postcss" scoped>
55
+ @import url("@knime/styles/css/mixins.css");
56
+
57
+ .container {
58
+ position: relative;
59
+ background-color: var(--knime-white);
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ height: 100%;
64
+
65
+ & > svg {
66
+ @mixin svg-icon-size 24;
67
+
68
+ position: absolute;
69
+ stroke: var(--knime-dove-gray);
70
+ }
71
+ }
72
+ </style>
@@ -0,0 +1 @@
1
+ export type * from "./HubAvatar.vue";
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, ref } from "vue";
3
+ import { useTextareaAutosize } from "@vueuse/core";
4
+
5
+ import { Button, InputField, Label, TextArea } from "@knime/components";
6
+
7
+ const emit = defineEmits<{
8
+ create: [{ name: string; description: string }];
9
+ cancel: [];
10
+ }>();
11
+
12
+ const versionName = ref("");
13
+ const versionDescription = ref("");
14
+
15
+ const descriptionEditor = ref<InstanceType<typeof TextArea>>(null);
16
+
17
+ onMounted(() => {
18
+ useTextareaAutosize({
19
+ element: descriptionEditor.value?.$el.getElementsByTagName("textarea")[0],
20
+ input: versionDescription,
21
+ });
22
+ });
23
+
24
+ const doValidate = ref(false);
25
+
26
+ const isVersionNameInvalid = computed(() => {
27
+ return versionName.value.length <= 0 && doValidate.value;
28
+ });
29
+
30
+ const onCreate = async () => {
31
+ doValidate.value = true;
32
+ await nextTick();
33
+
34
+ if (!isVersionNameInvalid.value) {
35
+ emit("create", {
36
+ name: versionName.value,
37
+ description: versionDescription.value,
38
+ });
39
+ }
40
+ };
41
+ </script>
42
+
43
+ <template>
44
+ <div class="create-version-form">
45
+ <Label text="Version name">
46
+ <InputField
47
+ v-model="versionName"
48
+ type="text"
49
+ required
50
+ :is-valid="!isVersionNameInvalid"
51
+ />
52
+ <div v-if="isVersionNameInvalid" class="error">
53
+ Please enter at least 1 character.
54
+ </div>
55
+ </Label>
56
+ <Label text="Description">
57
+ <TextArea
58
+ ref="descriptionEditor"
59
+ v-model="versionDescription"
60
+ class="description"
61
+ />
62
+ </Label>
63
+ <div class="controls">
64
+ <Button
65
+ with-border
66
+ compact
67
+ class="button cancel"
68
+ @click="$emit('cancel')"
69
+ >
70
+ <strong>Cancel</strong>
71
+ </Button>
72
+ <Button
73
+ primary
74
+ compact
75
+ class="button create"
76
+ :disabled="isVersionNameInvalid"
77
+ @click="onCreate"
78
+ >
79
+ <strong>Create</strong>
80
+ </Button>
81
+ </div>
82
+ </div>
83
+ </template>
84
+
85
+ <style lang="postcss" scoped>
86
+ .create-version-form {
87
+ height: 100%;
88
+ max-height: 100%;
89
+ width: 100%;
90
+ display: flex;
91
+ flex-direction: column;
92
+ padding: 30px 30px 0;
93
+ gap: 30px;
94
+ overscroll-behavior: none;
95
+ isolation: isolate;
96
+ overflow: auto;
97
+ z-index: 1;
98
+ background-color: var(--knime-gray-ultra-light);
99
+
100
+ & .description {
101
+ width: 100%;
102
+ max-width: unset;
103
+ display: flex;
104
+
105
+ & :deep(textarea) {
106
+ width: 100%;
107
+ box-sizing: content-box;
108
+ max-height: 200px;
109
+ resize: none;
110
+ }
111
+ }
112
+
113
+ & .error {
114
+ position: absolute;
115
+ word-break: keep-all;
116
+ margin-top: 6px;
117
+ font-weight: 300;
118
+ font-size: 13px;
119
+ color: var(--theme-color-error);
120
+ line-height: 18px;
121
+ }
122
+
123
+ & .controls {
124
+ border-top: 1px solid var(--knime-silver-sand);
125
+ display: flex;
126
+ margin: 0 -30px;
127
+ padding: 10px 20px;
128
+ gap: 10px;
129
+ justify-content: space-between;
130
+ margin-top: auto;
131
+ }
132
+ }
133
+ </style>