@bimdata/bcf-components 6.5.2 → 6.6.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/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  <h1 align="center">BIMData BCF components library</h1>
2
2
 
3
- A set (library) of Vue components to manage BCF and build BCF related
4
- features into your app.
3
+ A set of Vue components to manage BCF and build BCF related features into your app.
5
4
 
6
5
  Made with :heart: by [BIMData.io](https://bimdata.io/).
7
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bimdata/bcf-components",
3
- "version": "6.5.2",
3
+ "version": "6.6.0",
4
4
  "files": [
5
5
  "src",
6
6
  "vue3-plugin.js"
@@ -23,10 +23,10 @@
23
23
  "@semantic-release/changelog": "^6.0.3",
24
24
  "@semantic-release/commit-analyzer": "^13.0.1",
25
25
  "@semantic-release/git": "^10.0.1",
26
- "@semantic-release/github": "^11.0.1",
27
- "@semantic-release/npm": "^12.0.1",
28
- "@semantic-release/release-notes-generator": "^14.0.3",
26
+ "@semantic-release/github": "^11.0.6",
27
+ "@semantic-release/npm": "^12.0.2",
28
+ "@semantic-release/release-notes-generator": "^14.1.0",
29
29
  "conventional-changelog-eslint": "^6.0.0",
30
- "semantic-release": "^24.2.3"
30
+ "semantic-release": "^24.2.9"
31
31
  }
32
32
  }
@@ -44,6 +44,15 @@
44
44
  v-model="filters.statuses"
45
45
  />
46
46
 
47
+ <BIMDataSelect
48
+ width="100%"
49
+ :multi="true"
50
+ :label="$t('BcfComponents.BcfFilters.stageLabel')"
51
+ :nullLabel="$t('BcfComponents.BcfFilters.undefined')"
52
+ :options="stageOptions"
53
+ v-model="filters.stages"
54
+ />
55
+
47
56
  <div class="bcf-filters__container__date">
48
57
  <BIMDataDatePicker
49
58
  v-model="filters.startDate"
@@ -158,6 +167,10 @@ export default {
158
167
  getSelectOptions(props.topics.map((topic) => topic.topic_status))
159
168
  );
160
169
 
170
+ const stageOptions = computed(() =>
171
+ getSelectOptions(props.topics.map((topic) => topic.stage))
172
+ );
173
+
161
174
  const userOptions = computed(() =>
162
175
  getSelectOptions(props.topics.map((topic) => topic.assigned_to))
163
176
  );
@@ -195,6 +208,7 @@ export default {
195
208
  labelOptions,
196
209
  priorityOptions,
197
210
  statusOptions,
211
+ stageOptions,
198
212
  userOptions,
199
213
  // Methods
200
214
  close,
@@ -159,6 +159,11 @@
159
159
  v-model="topicLabels"
160
160
  :multi="true"
161
161
  />
162
+ <BcfTopicDocumentsSelector
163
+ :project="project"
164
+ :documents="topicDocuments"
165
+ @selection-change="topicDocuments = $event"
166
+ />
162
167
  </div>
163
168
  </div>
164
169
 
@@ -201,12 +206,14 @@ import service from "../../service.js";
201
206
  import { setViewpointDefaults } from "../../utils/viewpoints.js";
202
207
  import { getViewerViewpoint } from "../../utils/viewer.js";
203
208
  // Components
209
+ import BcfTopicDocumentsSelector from "./bcf-topic-documents-selector/BcfTopicDocumentsSelector.vue";
204
210
  import BcfTopicImages from "./bcf-topic-images/BcfTopicImages.vue";
205
211
  import BcfTopicSnapshots from "./bcf-topic-snapshots/BcfTopicSnapshots.vue";
206
212
  import BcfTopicSnapshotsActions from "./bcf-topic-snapshots-actions/BcfTopicSnapshotsActions.vue";
207
213
 
208
214
  export default {
209
215
  components: {
216
+ BcfTopicDocumentsSelector,
210
217
  BcfTopicImages,
211
218
  BcfTopicSnapshots,
212
219
  BcfTopicSnapshotsActions,
@@ -289,6 +296,7 @@ export default {
289
296
  const topicDueDate = ref(null);
290
297
  const topicDescription = ref("");
291
298
  const topicLabels = ref([]);
299
+ const topicDocuments = ref([]);
292
300
  const viewpoints = ref([]);
293
301
 
294
302
  const viewpointsToCreate = ref([]);
@@ -304,8 +312,8 @@ export default {
304
312
  let viewerLayout = null;
305
313
 
306
314
  const hasErrorTitle = ref(false);
307
- const isOpenModal = ref(false);
308
315
  const loading = ref(false);
316
+ const isOpenModal = ref(false);
309
317
 
310
318
  const reset = () => {
311
319
  topicTitle.value = "";
@@ -317,6 +325,7 @@ export default {
317
325
  topicDueDate.value = null;
318
326
  topicDescription.value = "";
319
327
  topicLabels.value = [];
328
+ topicDocuments.value = [];
320
329
  viewpoints.value = [];
321
330
  viewpointsToCreate.value = [];
322
331
  viewpointsToUpdate.value = [];
@@ -339,6 +348,7 @@ export default {
339
348
  topicDueDate.value = topic.due_date;
340
349
  topicDescription.value = topic.description || "";
341
350
  topicLabels.value = topic.labels || [];
351
+ topicDocuments.value = topic.documents || [];
342
352
  viewpoints.value = topic.viewpoints || [];
343
353
  } else {
344
354
  reset();
@@ -471,6 +481,8 @@ export default {
471
481
  newTopic = await service.updateTopic(props.project, data);
472
482
  }
473
483
 
484
+ await service.updateTopicDocuments(props.project, newTopic, topicDocuments.value);
485
+
474
486
  await Promise.all([
475
487
  ...viewpointsToCreate.value.map((viewpoint) =>
476
488
  service.createViewpoint(props.project, newTopic, viewpoint)
@@ -506,6 +518,7 @@ export default {
506
518
  nextIndex,
507
519
  topicAssignedTo,
508
520
  topicDescription,
521
+ topicDocuments,
509
522
  topicDueDate,
510
523
  topicPriority,
511
524
  topicStage,
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <div class="bcf-topic-documents-selector" @click="isOpenDMS = true">
3
+ <span class="label">
4
+ <template v-if="documents.length > 0">
5
+ {{ $t("BcfComponents.BcfTopicForm.documentsCount", { count: documents.length }) }}
6
+ </template>
7
+ <template v-else>
8
+ {{ $t("BcfComponents.BcfTopicForm.documentsLabel") }}
9
+ </template>
10
+ </span>
11
+ <BIMDataIconChevron size="xxs" />
12
+ <div class="underline"></div>
13
+ </div>
14
+
15
+ <div v-if="isOpenDMS" class="bcf-topic-form__dms">
16
+ <BIMDataButton ghost radius @click="isOpenDMS = false">
17
+ <BIMDataIconArrow size="xxs" />
18
+ <span class="m-l-6">{{ "BcfComponents.back" }}</span>
19
+ </BIMDataButton>
20
+ <BIMDataFileManager
21
+ :locale="$i18n.locale"
22
+ :headerButtons="false"
23
+ :apiUrl="apiUrl()"
24
+ :archiveUrl="''"
25
+ :accessToken="accessToken()"
26
+ :spaceId="project.cloud.id"
27
+ :projectId="project.id"
28
+ :select="true"
29
+ :multi="true"
30
+ :init-selection="selectedDocuments"
31
+ @selection-change="selectedDocuments = $event"
32
+ />
33
+ <BIMDataButton
34
+ width="100%"
35
+ color="primary"
36
+ fill
37
+ radius
38
+ @click="isOpenDMS = false, $emit('selection-change', topicDocuments)"
39
+ >
40
+ {{ $t("BcfComponents.BcfTopicForm.validateDocuments") }}
41
+ </BIMDataButton>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup>
46
+ import { computed, onMounted, ref } from "vue";
47
+ import service from "../../../service.js";
48
+
49
+ const props = defineProps({
50
+ project: {
51
+ type: Object,
52
+ required: true,
53
+ },
54
+ documents: {
55
+ type: Array,
56
+ required: true,
57
+ }
58
+ });
59
+
60
+ defineEmits(["selection-change"]);
61
+
62
+ const apiUrl = () => service.apiClient.config.configuration.basePath;
63
+ const accessToken = () => service.apiClient.accessToken;
64
+
65
+ const isOpenDMS = ref(false);
66
+ const selectedDocuments = ref([]);
67
+
68
+ const topicDocuments = computed(
69
+ () => selectedDocuments.value.map(({ document }) => ({ guid: document.id }))
70
+ );
71
+
72
+ onMounted(() => {
73
+ selectedDocuments.value = props.documents.map(doc => ({ document: { id: doc.guid } }));
74
+ });
75
+ </script>
76
+
77
+ <style scoped>
78
+ .bcf-topic-documents-selector {
79
+ position: relative;
80
+ height: 32px;
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ cursor: pointer;
85
+
86
+ .label {
87
+ font-size: 12px;
88
+ color: var(--color-granite-light);
89
+ }
90
+
91
+ .underline {
92
+ position: absolute;
93
+ bottom: 0;
94
+ width: 100%;
95
+ height: 1px;
96
+ display: block;
97
+ background: var(--color-silver);
98
+ }
99
+ }
100
+
101
+ .bcf-topic-form__dms:deep() {
102
+ position: absolute;
103
+ z-index: 2;
104
+ top: 0;
105
+ left: 0;
106
+ width: 100%;
107
+ height: 100%;
108
+ padding: var(--spacing-unit);
109
+ background-color: var(--color-white);
110
+
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: flex-start;
114
+ gap: var(--spacing-unit);
115
+
116
+ .bimdata-file-manager {
117
+ flex: 1;
118
+ overflow: auto;
119
+
120
+ .bimdata-responsive-grid {
121
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important;
122
+ }
123
+
124
+ .file-card {
125
+ height: 150px;
126
+
127
+ .file-card__content__footer__name {
128
+ --textbox-lines: 1 !important;
129
+
130
+ .multi-line-text-box__content__full,
131
+ .multi-line-text-box__ghost {
132
+ display: block ruby;
133
+ overflow: hidden;
134
+ text-overflow: ellipsis;
135
+ }
136
+ }
137
+
138
+ .file-card__content__footer__initials {
139
+ display: none;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ </style>
@@ -42,7 +42,7 @@
42
42
  class="bcf-topic-overview__content__head__index"
43
43
  :style="{
44
44
  backgroundColor: `#${priorityColor}`,
45
- color: adjustTextColor(`#${priorityColor}`, '#FFF', '#2F374A'),
45
+ color: adjustTextColor(`#${priorityColor}`, '#FFF', 'var(--color-text)'),
46
46
  }"
47
47
  >
48
48
  {{ topic.index }}
@@ -198,6 +198,12 @@
198
198
  </div>
199
199
  </div>
200
200
 
201
+ <BcfTopicDocuments
202
+ :project="project"
203
+ :topic="topic"
204
+ @view-topic-document="$emit('view-topic-document', $event)"
205
+ />
206
+
201
207
  <BcfTopicComments
202
208
  :uiConfig="uiConfig"
203
209
  :project="project"
@@ -232,18 +238,20 @@
232
238
 
233
239
  <script>
234
240
  import { adjustTextColor } from "@bimdata/design-system/src/BIMDataComponents/BIMDataColorSelector/colors.js";
235
- import { computed, onMounted, ref } from "vue";
241
+ import { computed, ref } from "vue";
236
242
  import service from "../../service.js";
237
243
  import { getPriorityColor } from "../../utils/topic.js";
238
244
  // Components
239
245
  import BcfTopicComments from "./bcf-topic-comments/BcfTopicComments.vue";
240
246
  import BcfTopicDefaultImage from "../bcf-topic-card/BcfTopicDefaultImage.vue";
247
+ import BcfTopicDocuments from "./bcf-topic-documents/BcfTopicDocuments.vue";
241
248
  import BcfTopicViewpoints from "./bcf-topic-viewpoints/BcfTopicViewpoints.vue";
242
249
 
243
250
  export default {
244
251
  components: {
245
252
  BcfTopicComments,
246
253
  BcfTopicDefaultImage,
254
+ BcfTopicDocuments,
247
255
  BcfTopicViewpoints,
248
256
  },
249
257
  props: {
@@ -291,6 +299,7 @@ export default {
291
299
  "view-comment-snapshot",
292
300
  "view-topic",
293
301
  "view-topic-components",
302
+ "view-topic-document",
294
303
  "view-topic-viewpoint",
295
304
  ],
296
305
  setup(props, { emit }) {
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="bcf-topic-documents">
3
+ <div
4
+ class="bcf-topic-documents__item"
5
+ v-for="doc in documents"
6
+ :key="doc.guid"
7
+ @click="$emit('view-topic-document', doc)"
8
+ >
9
+ <BIMDataFileIcon :size="22" :file-name="doc.referenced_document" />
10
+ <span>{{ doc.referenced_document }}</span>
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup>
16
+ import { computed, watch } from "vue";
17
+ import service from "../../../service.js";
18
+
19
+ const props = defineProps({
20
+ project: {
21
+ type: Object,
22
+ required: true,
23
+ },
24
+ topic: {
25
+ type: Object,
26
+ required: true,
27
+ },
28
+ });
29
+
30
+ defineEmits(["view-topic-document"]);
31
+
32
+ const documents = computed(() => props.topic.documents ?? []);
33
+
34
+ watch(
35
+ () => props.topic,
36
+ async () => {
37
+ if (!props.topic.documents) {
38
+ // Load topic documents if they are not loaded yet
39
+ props.topic.documents = await service.fetchTopicDocuments(props.project, props.topic);
40
+ }
41
+ },
42
+ { immediate: true }
43
+ );
44
+ </script>
45
+
46
+ <style scoped>
47
+ .bcf-topic-documents {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--spacing-unit);
51
+ }
52
+
53
+ .bcf-topic-documents__item {
54
+ padding: calc(var(--spacing-unit) / 2) var(--spacing-unit);
55
+ border-radius: 3px;
56
+ background-color: var(--color-silver-light);
57
+ display: flex;
58
+ align-items: center;
59
+ gap: var(--spacing-unit);
60
+ font-weight: bold;
61
+ cursor: pointer;
62
+ }
63
+ </style>
@@ -5,7 +5,7 @@
5
5
  class="status-badge"
6
6
  :style="{
7
7
  backgroundColor: `#${statusColor}`,
8
- color: adjustTextColor(`#${statusColor}`, '#FFF', '#2F374A'),
8
+ color: adjustTextColor(`#${statusColor}`, '#FFF', 'var(--color-text)'),
9
9
  }"
10
10
  >
11
11
  <BIMDataIconInformation fill color="default" />
@@ -6,7 +6,7 @@
6
6
  displayedColumns
7
7
  .map(col => ({
8
8
  ...col,
9
- label: col.label || $t(`BcfComponents.BcfTopicsTable.headers.${col.id}`)
9
+ label: col.label ?? $t(`BcfComponents.BcfTopicsTable.headers.${col.id}`)
10
10
  }))
11
11
  "
12
12
  :rows="topics"
@@ -54,7 +54,12 @@
54
54
  {{ toShortDateFormat(topic.creation_date) }}
55
55
  </template>
56
56
  <template #cell-actions="{ row: topic }">
57
- <BcfTopicActionsCell :topic="topic" @open-topic="$emit('open-topic', $event)" :warning="warningCallback(topic)" :warningTooltipMessage="warningTooltipMessage" />
57
+ <BcfTopicActionsCell
58
+ :topic="topic"
59
+ @open-topic="$emit('open-topic', $event)"
60
+ :warning="warningCallback(topic)"
61
+ :warningTooltipMessage="warningTooltipMessage"
62
+ />
58
63
  </template>
59
64
  </BIMDataTable>
60
65
  </template>
@@ -3,7 +3,7 @@
3
3
  class="bcf-topic-status-cell"
4
4
  :style="{
5
5
  backgroundColor: `#${statusColor}`,
6
- color: adjustTextColor(statusColor, '#ffffff', 'var(--color-text)')
6
+ color: adjustTextColor(statusColor, '#FFF', 'var(--color-text)')
7
7
  }"
8
8
  >
9
9
  {{ topic.topic_status }}
@@ -3,6 +3,7 @@ import { computed, reactive } from "vue";
3
3
  const EMPTY_FILTERS = {
4
4
  priorities: [],
5
5
  statuses: [],
6
+ stages: [],
6
7
  users: [],
7
8
  creators: [],
8
9
  labels: [],
@@ -21,6 +22,9 @@ function useBcfFilter(topics) {
21
22
  if (filters.statuses.length > 0) {
22
23
  list = list.filter(t => filters.statuses.includes(t.topic_status));
23
24
  }
25
+ if (filters.stages.length > 0) {
26
+ list = list.filter(t => filters.stages.includes(t.stage));
27
+ }
24
28
  if (filters.users.length > 0) {
25
29
  list = list.filter(t => filters.users.includes(t.assigned_to));
26
30
  }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Zurück",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filter",
5
6
  "filtersTitle": "Filter",
@@ -14,7 +15,6 @@
14
15
  "datePlaceholder": "Erstellungsdatum"
15
16
  },
16
17
  "BcfSettings": {
17
- "goBackButton": "Zurück",
18
18
  "title": "BCF-Parameter",
19
19
  "text": "Erstellen, füllen und listen Sie die BCF-Einstellungen auf, die Sie für dieses Projekt haben möchten.",
20
20
  "validateButton": "Die BCF-Parameter anerkennen"
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Back",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filters",
5
6
  "filtersTitle": "Filters",
@@ -14,7 +15,6 @@
14
15
  "datePlaceholder": "Creation date"
15
16
  },
16
17
  "BcfSettings": {
17
- "goBackButton": "Back",
18
18
  "title": "BCF Settings",
19
19
  "text": "Create, enter in, and list the BCF settings you would like to have for this project",
20
20
  "validateButton": "Validate BCF settings"
@@ -69,7 +69,10 @@
69
69
  "takeSnapshot": "Take a snapshot",
70
70
  "importFile": "Import a file",
71
71
  "searchPlaceholder": "Search",
72
- "emptySearch": "No result"
72
+ "emptySearch": "No result",
73
+ "documentsLabel": "Add a file",
74
+ "documentsCount": "Files - {count} selected files",
75
+ "validateDocuments": "Validate"
73
76
  },
74
77
  "BcfTopicOverview": {
75
78
  "openViewer": "Open in viewer",
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Volver",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filtros",
5
6
  "filtersTitle": "Filtros",
@@ -14,7 +15,6 @@
14
15
  "datePlaceholder": "Fecha de creación"
15
16
  },
16
17
  "BcfSettings": {
17
- "goBackButton": "Volver",
18
18
  "title": "Parámetros del BCF",
19
19
  "text": "Cree, rellene y enumere los parámetros del BCF que desea tener en este proyecto",
20
20
  "validateButton": "Validar los parámetros del BCF"
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Retour",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filtres",
5
6
  "filtersTitle": "Filtres",
6
7
  "priorityLabel": "Priorité",
7
8
  "statusLabel": "Statut",
9
+ "stageLabel": "Phase",
8
10
  "assignedToLabel": "Assigné à",
9
11
  "creatorsLabel": "Créé par",
10
- "tagsLabel": "Tags",
12
+ "tagsLabel": "Tags",
11
13
  "resetButton": "Réinitialiser",
12
14
  "searchButton": "Rechercher",
13
15
  "undefined": "Non défini",
14
16
  "datePlaceholder": "Date de création"
15
17
  },
16
18
  "BcfSettings": {
17
- "goBackButton": "Retour",
18
19
  "title": "Paramètres BCF",
19
20
  "text": "Créez, renseignez et listez les paramètres BCF que vous souhaitez avoir sur ce projet",
20
21
  "validateButton": "Valider les paramètres BCF"
@@ -69,7 +70,10 @@
69
70
  "takeSnapshot": "Prendre un snapshot",
70
71
  "importFile": "Importer un fichier",
71
72
  "searchPlaceholder": "Rechercher",
72
- "emptySearch": "Pas de résultat"
73
+ "emptySearch": "Pas de résultat",
74
+ "documentsLabel": "Documents",
75
+ "documentsCount": "Documents - {count} documents sélectionnés",
76
+ "validateDocuments": "Valider"
73
77
  },
74
78
  "BcfTopicOverview": {
75
79
  "openViewer": "Ouvrir dans le viewer",
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Indietro",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filtri",
5
6
  "filtersTitle": "Filtri",
@@ -14,7 +15,6 @@
14
15
  "undefined": "Non definito"
15
16
  },
16
17
  "BcfSettings": {
17
- "goBackButton": "Indietro",
18
18
  "title": "Parametri BCF",
19
19
  "text": "Creare, notificare e elencare i parametri BCF che si desidera avere su questo progetto",
20
20
  "validateButton": "Conferma i parametri BCF"
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Terug",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filters",
5
6
  "filtersTitle": "Filters",
@@ -14,7 +15,6 @@
14
15
  "undefined": "Niet gedefinieerd"
15
16
  },
16
17
  "BcfSettings": {
17
- "goBackButton": "Terug",
18
18
  "title": "BCF-instellingen",
19
19
  "text": "De voor dit project gewenste BCF-instellingen maken, invoeren en in een lijst plaatsen",
20
20
  "validateButton": "BCF-instellingen valideren"
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "BcfComponents": {
3
+ "back": "Tilbake",
3
4
  "BcfFilters": {
4
5
  "filtersButton": "Filtre",
5
6
  "filtersTitle": "Filtre",
@@ -14,7 +15,6 @@
14
15
  "undefined": "Ikke definert"
15
16
  },
16
17
  "BcfSettings": {
17
- "goBackButton": "Tilbake",
18
18
  "title": "BCF-innstillinger",
19
19
  "text": "Opprett, fyll inn og list opp BCF-innstillingene du vil ha til dette prosjektet",
20
20
  "validateButton": "Valider BCF-innstillinger"
package/src/service.js CHANGED
@@ -113,6 +113,20 @@ class Service {
113
113
  downloadBlobAs(`${project.name}.xlsx`, response);
114
114
  }
115
115
 
116
+ // --- BCF Topic Documents API
117
+
118
+ fetchTopicDocuments(project, topic) {
119
+ return this.apiClient.bcfApi.getDocumentReferences(topic.guid, project.id);
120
+ }
121
+
122
+ createTopicDocument(project, topic, document) {
123
+ return this.apiClient.bcfApi.createDocumentReference(topic.guid, project.id, document);
124
+ }
125
+
126
+ updateTopicDocuments(project, topic, documents) {
127
+ return this.apiClient.bcfApi.fullUpdateDocumentReference(topic.guid, project.id, documents);
128
+ }
129
+
116
130
  // --- BCF Topic Viewpoints API ---
117
131
 
118
132
  async loadTopicsViewpoints(project, topics) {