Merge branch 'develop' into feat-scim

This commit is contained in:
Ronni Skansing
2026-03-28 15:17:22 +01:00
10 changed files with 605 additions and 50 deletions
+38
View File
@@ -2516,6 +2516,25 @@ export class API {
});
},
/**
* Create a dynamic recipient group.
*
* @param {string} name
* @param {string|null} companyID
* @param {string} filterField - recipient attribute to filter on (position, department, city, country, misc)
* @param {string} filterValue - value to match against the filter field
* @returns {Promise<ApiResponse>}
*/
createDynamicGroup: async (name, companyID, filterField, filterValue) => {
return await postJSON(this.getPath('/recipient/group'), {
name: name,
companyID: companyID,
isDynamic: true,
filterField: filterField,
filterValue: filterValue
});
},
/**
* Update a recipient group - not the recipients in the group.
*
@@ -2532,6 +2551,25 @@ export class API {
});
},
/**
* Update a dynamic recipient group.
*
* @param {object} group
* @param {string} group.id
* @param {string} [group.name]
* @param {string} [group.filterField]
* @param {string} [group.filterValue]
* @returns {Promise<ApiResponse>}
*/
updateDynamicGroup: async ({ id, name, filterField, filterValue }) => {
return await patchJSON(this.getPath(`/recipient/group/${id}`), {
name: name,
isDynamic: true,
filterField: filterField,
filterValue: filterValue
});
},
/**
* Get all recipient groups using pagination.
*
@@ -0,0 +1,6 @@
<span
title="Dynamic group"
class="select-none px-1 border-2 border-gray-400 dark:border-gray-500 relative -top-1 rounded-lg text-xs text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 transition-colors duration-200"
>
dynamic
</span>
+212 -13
View File
@@ -6,6 +6,8 @@
import { goto } from '$app/navigation';
import Headline from '$lib/components/Headline.svelte';
import TextField from '$lib/components/TextField.svelte';
import DynamicLabel from '$lib/components/DynamicLabel.svelte';
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
import TableRow from '$lib/components/table/TableRow.svelte';
import TableCell from '$lib/components/table/TableCell.svelte';
import TableCellLink from '$lib/components/table/TableCellLink.svelte';
@@ -33,15 +35,29 @@
// services
const appStateService = AppStateService.instance;
// the recipient fields that can be used as a dynamic filter
const dynamicFilterFields = ['position', 'department', 'city', 'country', 'misc'];
// data
let form = null;
let dynamicForm = null;
let modalError = '';
let dynamicModalError = '';
let formValues = {
name: null,
companyID: null,
recipients: []
};
let dynamicFormValues = {
name: null,
filterField: dynamicFilterFields[0],
filterValue: null
};
let isModalVisible = false;
let isDynamicModalVisible = false;
let isSubmitting = false;
let isTableLoading = false;
const tableURLParams = newTableURLParams();
@@ -57,6 +73,17 @@
name: null
};
// dynamic update modal state
let isDynamicUpdateModalVisible = false;
let dynamicUpdateForm = null;
let dynamicUpdateError = '';
let dynamicUpdateValues = {
id: null,
name: null,
filterField: dynamicFilterFields[0],
filterValue: null
};
$: {
modalText = modalMode === 'create' ? 'New group' : 'Update group';
}
@@ -162,6 +189,54 @@
}
};
const onSubmitDynamicCreate = async () => {
try {
isSubmitting = true;
const res = await api.recipient.createDynamicGroup(
dynamicFormValues.name,
contextCompanyID,
dynamicFormValues.filterField,
dynamicFormValues.filterValue
);
if (!res.success) {
dynamicModalError = res.error;
return;
}
addToast('Dynamic group created', 'Success');
refreshGroups();
closeDynamicModal();
} catch (e) {
addToast('Failed to create dynamic group', 'Error');
console.error('failed to create dynamic group', e);
} finally {
isSubmitting = false;
}
};
const onSubmitDynamicUpdate = async () => {
try {
isSubmitting = true;
const res = await api.recipient.updateDynamicGroup({
id: dynamicUpdateValues.id,
name: dynamicUpdateValues.name,
filterField: dynamicUpdateValues.filterField,
filterValue: dynamicUpdateValues.filterValue
});
if (!res.success) {
dynamicUpdateError = res.error;
return;
}
addToast('Dynamic group updated', 'Success');
refreshGroups();
closeDynamicUpdateModal();
} catch (err) {
addToast('Failed to update dynamic group', 'Error');
console.error('failed to update dynamic group', err);
} finally {
isSubmitting = false;
}
};
const openDeleteAlert = async (group) => {
isDeleteAlertVisible = true;
deleteValues.id = group.id;
@@ -195,6 +270,16 @@
isModalVisible = true;
};
const openDynamicCreateModal = () => {
dynamicFormValues = {
name: null,
filterField: dynamicFilterFields[0],
filterValue: null
};
dynamicModalError = '';
isDynamicModalVisible = true;
};
/** @param {string} id */
const openUpdateModal = async (id) => {
modalMode = 'update';
@@ -212,11 +297,42 @@
}
};
/** @param {string} id */
const openDynamicUpdateModal = async (id) => {
try {
showIsLoading();
const group = await loadGroup(id);
dynamicUpdateValues.id = id;
dynamicUpdateValues.name = group.name;
dynamicUpdateValues.filterField = group.filterField ?? dynamicFilterFields[0];
dynamicUpdateValues.filterValue = group.filterValue ?? '';
dynamicUpdateError = '';
isDynamicUpdateModalVisible = true;
} catch (e) {
addToast('Failed to load group', 'Error');
console.error('failed to load group', e);
} finally {
hideIsLoading();
}
};
const closeModal = () => {
isModalVisible = false;
form.reset();
modalError = '';
};
const closeDynamicModal = () => {
isDynamicModalVisible = false;
if (dynamicForm) dynamicForm.reset();
dynamicModalError = '';
};
const closeDynamicUpdateModal = () => {
isDynamicUpdateModalVisible = false;
if (dynamicUpdateForm) dynamicUpdateForm.reset();
dynamicUpdateError = '';
};
</script>
<HeadTitle title="Groups" />
@@ -224,6 +340,7 @@
<Headline>Groups</Headline>
<div class="flex gap-3">
<BigButton on:click={openCreateModal}>New group</BigButton>
<BigButton on:click={openDynamicCreateModal}>New dynamic group</BigButton>
<BigButton on:click={() => goto('/recipient/orphaned/')}>View Orphaned</BigButton>
</div>
<Table
@@ -242,6 +359,9 @@
{#each groups as group}
<TableRow>
<TableCellLink href={`/recipient/group/${group.id}`} title={group.name}>
{#if group.isDynamic}
<DynamicLabel />
{/if}
{group.name}
{#if group.scimEnabled}
<span
@@ -255,6 +375,7 @@
<TableCellLink href={`/recipient/group/${group.id}`} title={group.recipientCount}>
{group.recipientCount}
</TableCellLink>
{#if contextCompanyID}
<TableCellScope companyID={group.companyID} />
{/if}
@@ -266,10 +387,17 @@
on:click={() => gotoEditGroupRecipients(group.id)}
{...globalButtonDisabledAttributes(group, contextCompanyID)}
/>
<TableUpdateButton
on:click={() => openUpdateModal(group.id)}
{...globalButtonDisabledAttributes(group, contextCompanyID)}
/>
{#if group.isDynamic}
<TableUpdateButton
on:click={() => openDynamicUpdateModal(group.id)}
{...globalButtonDisabledAttributes(group, contextCompanyID)}
/>
{:else}
<TableUpdateButton
on:click={() => openUpdateModal(group.id)}
{...globalButtonDisabledAttributes(group, contextCompanyID)}
/>
{/if}
<TableDeleteButton
on:click={() => openDeleteAlert(group)}
{...globalButtonDisabledAttributes(group, contextCompanyID)}
@@ -280,6 +408,7 @@
{/each}
</Table>
<!-- static group modal -->
<Modal headerText={modalText} bind:visible={isModalVisible} onClose={closeModal} {isSubmitting}>
<FormGrid on:submit={onSubmit} bind:bindTo={form} {isSubmitting}>
<FormColumns>
@@ -291,21 +420,91 @@
bind:value={formValues.name}
placeholder="Marketing">Name</TextField
>
<section>
{#each formValues.recipients as recipient}
<div class="row">
<div class="col-12">
{recipient.email}
</div>
</div>
{/each}
</section>
</FormColumn>
</FormColumns>
<FormError message={modalError} />
<FormFooter {closeModal} {isSubmitting} />
</FormGrid>
</Modal>
<!-- dynamic group create modal -->
<Modal
headerText="New dynamic group"
bind:visible={isDynamicModalVisible}
onClose={closeDynamicModal}
{isSubmitting}
>
<FormGrid on:submit={onSubmitDynamicCreate} bind:bindTo={dynamicForm} {isSubmitting}>
<FormColumns>
<FormColumn>
<TextField
required
minLength={1}
maxLength={127}
bind:value={dynamicFormValues.name}
placeholder="Marketing">Name</TextField
>
<TextFieldSelect
id="dynamic-group-filter-field"
required
bind:value={dynamicFormValues.filterField}
options={dynamicFilterFields}
>
Filter field
</TextFieldSelect>
<TextField
required
minLength={1}
maxLength={127}
bind:value={dynamicFormValues.filterValue}
placeholder="e.g. Engineering">Filter value</TextField
>
</FormColumn>
</FormColumns>
<FormError message={dynamicModalError} />
<FormFooter closeModal={closeDynamicModal} {isSubmitting} />
</FormGrid>
</Modal>
<!-- dynamic group update modal -->
<Modal
headerText="Update dynamic group"
bind:visible={isDynamicUpdateModalVisible}
onClose={closeDynamicUpdateModal}
{isSubmitting}
>
<FormGrid on:submit={onSubmitDynamicUpdate} bind:bindTo={dynamicUpdateForm} {isSubmitting}>
<FormColumns>
<FormColumn>
<TextField
required
minLength={1}
maxLength={127}
bind:value={dynamicUpdateValues.name}
placeholder="Marketing">Name</TextField
>
<TextFieldSelect
id="dynamic-group-filter-field-a"
required
bind:value={dynamicUpdateValues.filterField}
options={dynamicFilterFields}
>
Filter field
</TextFieldSelect>
<TextField
required
minLength={1}
maxLength={127}
bind:value={dynamicUpdateValues.filterValue}
placeholder="e.g. Engineering">Filter value</TextField
>
</FormColumn>
</FormColumns>
<FormError message={dynamicUpdateError} />
<FormFooter closeModal={closeDynamicUpdateModal} {isSubmitting} />
</FormGrid>
</Modal>
<DeleteAlert
list={[
'All assets will be deleted',
@@ -1,5 +1,6 @@
<script>
import { onMount } from 'svelte';
import DynamicLabel from '$lib/components/DynamicLabel.svelte';
import { api } from '$lib/api/apiProxy.js';
import { newTableURLParams } from '$lib/service/tableURLParams.js';
import { page } from '$app/stores';
@@ -45,7 +46,10 @@
let groupValues = {
id: null,
name: null,
companyID: null
companyID: null,
isDynamic: false,
filterField: null,
filterValue: null
};
let recipientSearch = '';
let searchOptions = [];
@@ -112,6 +116,9 @@
groupValues.id = res.data.id;
groupValues.name = res.data.name;
groupValues.companyID = res.data.companyID;
groupValues.isDynamic = res.data.isDynamic ?? false;
groupValues.filterField = res.data.filterField ?? null;
groupValues.filterValue = res.data.filterValue ?? null;
} catch (err) {
addToast('Failed to load group', 'Error');
console.error('failed to load group', err);
@@ -395,16 +402,35 @@
<HeadTitle title="Group ({groupValues.name}" />
<main>
<Headline>Group Recipients</Headline>
<SubHeadline>{groupValues.name}</SubHeadline>
<BigButton
on:click={openAddRecipientsModal}
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Add Recipients</BigButton
>
<BigButton
on:click={openImportModal}
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Import from CSV</BigButton
>
<SubHeadline>Recipients</SubHeadline>
<SubHeadline>
{#if groupValues.isDynamic}
<DynamicLabel />
{/if}
{groupValues.name}
</SubHeadline>
{#if groupValues.isDynamic}
<p class="text-sm font-semibold text-slate-600 dark:text-gray-400 mt-2">Dynamic group</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8">
Recipients where
<span class="font-mono text-xs bg-gray-200 dark:bg-gray-700 px-1 rounded"
>{groupValues.filterField}</span
>
equals
<span class="font-mono text-xs bg-gray-200 dark:bg-gray-700 px-1 rounded"
>{groupValues.filterValue}</span
>
are included automatically.
</p>
{:else}
<BigButton
on:click={openAddRecipientsModal}
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Add Recipients</BigButton
>
<BigButton
on:click={openImportModal}
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}>Import from CSV</BigButton
>
{/if}
<Table
columns={[
{ column: 'Email', size: 'large' },
@@ -456,10 +482,12 @@
<TableCellEmpty />
<TableCellAction>
<TableDropDownEllipsis>
<TableDeleteButton
on:click={() => openDeleteAlert(recipient)}
{...globalButtonDisabledAttributes(recipient, contextCompanyID)}
></TableDeleteButton>
{#if !groupValues.isDynamic}
<TableDeleteButton
on:click={() => openDeleteAlert(recipient)}
{...globalButtonDisabledAttributes(groupValues, contextCompanyID)}
></TableDeleteButton>
{/if}
</TableDropDownEllipsis>
</TableCellAction>
</TableRow>