Skip to content

Commit

Permalink
[ui] Manage organizations aliases
Browse files Browse the repository at this point in the history
An organization's aliases can be added and
deleted both on the organizations table and
the single organization view.

Signed-off-by: Eva Millán <evamillan@bitergia.com>
  • Loading branch information
evamillan committed Feb 16, 2024
1 parent 99d2a61 commit 3138d75
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 42 deletions.
16 changes: 16 additions & 0 deletions releases/unreleased/organization-aliases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Organization aliases
category: added
author: Eva Millán <evamillan@bitergia.com>
issue: 857
notes: >
Organizations can be known by different names. To avoid
duplicates, organizations can have aliases.
Searching for an organization using one of its aliases returns
the organization.
When an organization is merged into another, its name becomes
an alias of the target organization.
If a name exists as an alias, no organization can be created
with that name and viceversa.
An organization's aliases can be added and deleted both on the
organizations table and the single organization view.
39 changes: 39 additions & 0 deletions ui/src/apollo/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,26 @@ const UPDATE_TASK = gql`
}
`;

const ADD_ALIAS = gql`
mutation addAlias($alias: String!, $organization: String!) {
addAlias(alias: $alias, organization: $organization) {
alias {
alias
}
}
}
`;

const DELETE_ALIAS = gql`
mutation deleteAlias($alias: String!) {
deleteAlias(alias: $alias) {
alias {
alias
}
}
}
`;

const tokenAuth = (apollo, username, password) => {
const response = apollo.mutate({
mutation: TOKEN_AUTH,
Expand Down Expand Up @@ -793,6 +813,23 @@ const updateTask = (apollo, taskId, data) => {
});
};

const addAlias = (apollo, alias, organization) => {
return apollo.mutate({
mutation: ADD_ALIAS,
variables: {
alias,
organization,
},
});
};

const deleteAlias = (apollo, alias) => {
return apollo.mutate({
mutation: DELETE_ALIAS,
variables: { alias },
});
};

export {
tokenAuth,
lockIndividual,
Expand Down Expand Up @@ -824,4 +861,6 @@ export {
scheduleTask,
deleteTask,
updateTask,
addAlias,
deleteAlias

Check warning on line 865 in ui/src/apollo/mutations.js

View workflow job for this annotation

GitHub Actions / Node 16.x Python 3.8

Insert `,`

Check warning on line 865 in ui/src/apollo/mutations.js

View workflow job for this annotation

GitHub Actions / Node 16.x Python 3.9

Insert `,`
};
9 changes: 9 additions & 0 deletions ui/src/apollo/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ const GET_PAGINATED_ORGANIZATIONS = gql`
domain
isTopDomain
}
aliases {
alias
}
}
pageInfo {
page
Expand Down Expand Up @@ -253,6 +256,9 @@ const GET_ORGANIZATION = gql`
domain
isTopDomain
}
aliases {
alias
}
}
}
}
Expand Down Expand Up @@ -284,6 +290,9 @@ const FIND_ORGANIZATION = gql`
entities {
id
name
aliases {
alias
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/OrganizationEntry.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</template>
<v-list dense>
<v-list-item @click="$emit('edit')">
<v-list-item-title> View/edit domains </v-list-item-title>
<v-list-item-title> Edit domains and aliases </v-list-item-title>
</v-list-item>
<v-edit-dialog
large
Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/OrganizationModal.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const OrganizationModalTemplate = `
:is-open.sync="isOpen"
:add-domain="addDomain"
:add-organization="addDomain"
:delete-domain="addDomain" l
:delete-domain="addDomain"
:add-alias="() => {}"
:delete-alias="() => {}"
/>
</div>
`;
Expand Down
144 changes: 136 additions & 8 deletions ui/src/components/OrganizationModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,55 @@
</v-btn>
</v-col>
</v-row>
<v-row class="pl-3 mt-2">
<v-btn text small left outlined color="primary" @click="addInput">
<v-icon small color="primary">mdi-plus-circle-outline</v-icon>
<v-row class="pl-3 mt-2 mb-4">
<v-btn text small outlined color="primary" @click="addInput">
<v-icon small left color="primary">
mdi-plus-circle-outline
</v-icon>
Add domain
</v-btn>
</v-row>
<v-row class="pl-4">
<span class="subheader">Aliases</span>
</v-row>
<v-row
v-for="(alias, index) in form.aliases"
:key="`alias-${index}`"
class="align-bottom"
>
<v-col cols="10">
<v-text-field
v-model="form.aliases[index]"
label="Alias"
hide-details
outlined
dense
/>
</v-col>
<v-col cols="2" class="pt-3">
<v-btn
icon
color="primary"
@click="form.aliases.splice(index, 1)"
>
<v-icon color="primary">mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
<v-row class="pl-3 mt-2">
<v-btn
text
small
outlined
color="primary"
@click="form.aliases.push('')"
>
<v-icon small left color="primary">
mdi-plus-circle-outline
</v-icon>
Add alias
</v-btn>
</v-row>
<v-alert v-if="errorMessage" text type="error" class="mt-3">
{{ errorMessage }}
</v-alert>
Expand Down Expand Up @@ -114,12 +157,26 @@ export default {
required: false,
default: () => [],
},
aliases: {
type: Array,
required: false,
default: () => [""],
},
addAlias: {
type: Function,
required: true,
},
deleteAlias: {
type: Function,
required: true,
},
},
data() {
return {
form: {
name: "",
domains: [emptyDomain],
aliases: [""],
},
errorMessage: "",
savedData: {
Expand All @@ -133,6 +190,7 @@ export default {
this.form.domains.splice(index, 1);
},
async onSave() {
this.errorMessage = "";
if (!this.savedData.name) {
try {
const response = await this.addOrganization(this.form.name);
Expand All @@ -141,20 +199,30 @@ export default {
this.$logger.debug(`Added organization ${this.form.name}`);
if (
this.form.domains.length === 0 &&
this.savedData.domains.length === 0
this.savedData.domains.length === 0 &&
this.form.aliases.length === 0 &&
this.savedData.aliases.length === 0
) {
this.closeModal();
this.$emit("updateOrganizations");
return;
} else {
this.handleDomains();
await this.handleDomains();
await this.handleAliases();
if (!this.errorMessage) {
this.closeModal();
}
}
}
} catch (error) {
this.errorMessage = this.$getErrorMessage(error);
}
} else {
this.handleDomains();
await this.handleDomains();
await this.handleAliases();
if (!this.errorMessage) {
this.closeModal();
}
}
},
closeModal() {
Expand Down Expand Up @@ -186,7 +254,6 @@ export default {
deletedDomains.map((domain) => this.deleteOrganizationDomain(domain))
);
if (responseNew || responseDeleted) {
this.closeModal();
this.$emit("updateOrganizations");
}
} catch (error) {
Expand Down Expand Up @@ -225,23 +292,84 @@ export default {
addInput() {
this.form.domains.push({ ...emptyDomain });
},
async handleAliases() {
const newAliases = this.form.aliases.filter(
(alias) =>
alias.length > 0 &&
!this.savedData.aliases.some((savedAlias) => savedAlias === alias)
);
const deletedAliases = this.savedData.aliases.filter(
(alias) =>
alias.length > 0 &&
!this.form.aliases.some((savedAlias) => savedAlias === alias)
);
try {
const responseNew = await Promise.all(
newAliases.map((alias) =>
this.addOrganizationAlias(alias, this.form.name)
)
);
const responseDeleted = await Promise.all(
deletedAliases.map((alias) => this.deleteOrganizationAlias(alias))
);
if (responseNew || responseDeleted) {
this.$emit("updateOrganizations");
}
} catch (error) {
this.errorMessage = this.$getErrorMessage(error);
this.$logger.error(`Error updating aliases: ${error}`, {
organization: this.form.name,
newAliases,
deletedAliases,
});
}
},
async addOrganizationAlias(alias, organization) {
const response = await this.addAlias(alias, organization);
if (response && !response.error) {
this.savedData.aliases.push(alias);
this.$logger.debug(
`Added alias ${alias} to organization ${organization}`
);
return response;
} else if (response.errors) {
this.$logger.error(
`Error adding alias: ${response.errors[0].message}`,
{ alias, organization }
);
}
},
async deleteOrganizationAlias(alias) {
const response = await this.deleteAlias(alias);
if (response) {
this.$logger.debug(`Deleted alias ${alias}`);
return response;
}
},
},
watch: {
isOpen(value) {
if (value) {
Object.assign(this.savedData, {
name: this.organization,
domains: this.domains.map((domain) => ({ ...domain })),
aliases: this.aliases.map((alias) => alias),
});
Object.assign(this.form, {
name: this.organization || "",
domains: this.domains.map((domain) => ({ ...domain })),
aliases: this.aliases.map((alias) => alias),
});
} else {
Object.assign(this.form, { name: "", domains: [{ ...emptyDomain }] });
Object.assign(this.form, {
name: "",
domains: [{ ...emptyDomain }],
aliases: [""],
});
Object.assign(this.savedData, {
name: undefined,
domains: [{ ...emptyDomain }],
aliases: [""],
});
}
},
Expand Down
14 changes: 11 additions & 3 deletions ui/src/components/OrganizationSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
:search-input.sync="search"
:label="label"
:loading="isLoading"
:filter="filterItems"
item-text="name"
item-value="name"
:no-data-text="`No matches for &quot;${search}&quot;`"
:hide-no-data="!search || isLoading"
cache-items
clearable
dense
outlined
Expand Down Expand Up @@ -86,20 +86,28 @@ export default {
this.$emit("error", this.$getErrorMessage(error));
}
},
filterItems(item) {
// Return all items because the query is already filtered
return item;
},
},
computed: {
appendContent() {
return (
this.search &&
!this.organizations.some(
(org) => org.name.toLowerCase() === this.search.toLowerCase()
(org) =>
org.name.toLowerCase() === this.search.toLowerCase() ||
org.aliases.some(
(alias) => alias.alias.toLowerCase() === this.search.toLowerCase()
)
)
);
},
},
watch: {
search(value) {
if (value && value.length > 2) {
if (!value || value.length > 2) {
this.isLoading = true;
this.debounceSearch(value);
}
Expand Down
Loading

0 comments on commit 3138d75

Please sign in to comment.