Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: E2E
on: workflow_dispatch
on:
workflow_dispatch:
pull_request:
branches: ['v*/main']
permissions:
contents: read
jobs:
Expand All @@ -9,6 +12,10 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Setup Node.js
uses: actions/setup-node@v7
with:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Setup Node.js
uses: actions/setup-node@v7
with:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
[![Umbraco](https://img.shields.io/badge/marketplace-umbraco-%233544b1#f5c1bc)](https://marketplace.umbraco.com/package/usupport)

[![Release](https://github.com/Lantzify/uSupport/actions/workflows/main.yml/badge.svg)](https://github.com/Lantzify/uSupport/actions/workflows/main.yml)
[![Umbraco](https://img.shields.io/badge/our-umbraco-%233544b1)](https://our.umbraco.com/packages/backoffice-extensions/usupport/)
[![E2E](https://github.com/Lantzify/uSupport/actions/workflows/e2e.yml/badge.svg)](https://github.com/Lantzify/uSupport/actions/workflows/e2e.yml)

## What is uSupport
uSupport is an feature rich ticketing system designed specifically for Umbraco. It is a complete ticketing solution that enables users to create, manage, and view tickets and their associated comments. With uSupport, you can quickly and easily create and manage tickets. uSupport is highly customizable, allowing users to set up their own ticket types and define ticket statuses.
uSupport is a feature-rich ticketing system designed specifically for Umbraco. It is a complete ticketing solution that enables users to create, manage, and view tickets and their associated comments. With uSupport, you can quickly and easily create and manage tickets. uSupport is highly customizable, allowing users to set up their own ticket types and define ticket statuses.

## Why uSupport
Say goodbye to the days of digging through countless old emails in search of vital details. With uSupport, everything you need is neatly organized within the Umbraco environment. Streamline your workflow, boost productivity, and effortlessly access all the information you need to address and resolve client issues efficiently.
Expand Down
53 changes: 53 additions & 0 deletions src/uSupport.E2E/tests/admin/types/create.types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect } from '@playwright/test';
import { test } from '@umbraco-cms/acceptance-test-helpers';
import { TYPES, createTicket } from '../../helpers';

test('No umbraco property', async ({ page }) => {
const type = `${TYPES.request.name} ${Date.now()}`;
const ticket = `A request ${Date.now()}`;

await page.goto('/umbraco/section/usupport/workspace/usupport-ticket-type/create');
await page.getByRole('textbox', { name: 'Enter a name...' }).fill(type);
await page.getByRole('textbox', { name: 'Enter a description...' }).fill(TYPES.request.summary);

await page.getByRole('button', { name: 'Save' }).click();
await expect(page.locator('uui-toast-notification').getByText('Type saved')).toBeVisible();

await createTicket(page, type, ticket, TYPES.request.summary);

await expect(page.locator('usupport-tickets-table[list="active"]').getByRole('cell', { name: ticket })).toBeVisible();
});

test('Umbraco property', async ({ page }) => {
const type = `${TYPES.meetingRequest.name} ${Date.now()}`;
const ticket = `Serious meeting ${Date.now()}`;

await page.goto('/umbraco/section/usupport/workspace/usupport-ticket-type/create');
await page.getByRole('textbox', { name: 'Enter a name...' }).fill(type);
await page.getByRole('textbox', { name: 'Enter a description...' }).fill(TYPES.meetingRequest.summary);

await page.getByRole('button', { name: 'Choose' }).click();
await page.getByRole('button', { name: 'Date Picker with time' }).click();
await page.locator('#container').getByRole('button', { name: 'Choose' }).click();
await page.getByRole('textbox', { name: 'Property name' }).click();
await page.getByRole('textbox', { name: 'Property name' }).fill('Date');

await page.getByRole('button', { name: 'Save' }).click();
await expect(page.locator('uui-toast-notification').getByText('Type saved')).toBeVisible();

await page.goto('/umbraco/section/content');
await page.getByRole('tab', { name: 'Support', exact: true }).click();
await page.getByRole('button', { name: 'Create ticket' }).click();
await page.getByRole('button', { name: type }).click();

await page.getByRole('textbox', { name: 'Title' }).click();
await page.getByRole('textbox', { name: 'Title' }).fill(ticket);
await page.getByRole('textbox', { name: 'Summary' }).fill("Let's have a serious meeting.");


await page.getByRole('textbox', { name: 'Set a date...' }).fill('2026-11-18T13:17:50');
await page.locator('#container').getByRole('button', { name: 'Create ticket' }).click();
await expect(page.locator('uui-toast-notification').getByText(/was created/)).toBeVisible();

await expect(page.locator('usupport-tickets-table[list="active"]').getByRole('cell', { name: ticket })).toBeVisible();
});
11 changes: 11 additions & 0 deletions src/uSupport.E2E/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ export const TICKETS = {
type: 'Page error',
title: 'Home page throws error',
summary: 'When going to the home page it throws a 500 error'
},
};

export const TYPES = {
request: {
name: 'Feature request',
summary: 'I would like to request a new feature'
},
meetingRequest: {
name: 'Meeting request',
summary: 'I would like to request a meeting'
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/uSupport.Tests/Migrations/MigrateContentPickerValuesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using uSupport.Migrations.Updates._17._0._0;

namespace uSupport.Tests.Migrations
{
[TestFixture]
public class MigrateContentPickerValuesTests
{
private static readonly Guid Key = Guid.Parse("dcf18a51-6919-4cf8-89d1-36b94ce4d963");
private static readonly string Expected = $"\"{Key}\"";

private static Guid? IdToKey(int id) => id == 1234 ? Key : null;

[TestCase("1234")]
[TestCase("\"1234\"")]
[TestCase("\"umb://document/dcf18a5169194cf889d136b94ce4d963\"")]
[TestCase("umb://document/dcf18a5169194cf889d136b94ce4d963")]
public void Converts_v13_values_to_document_key(string value)
=> Assert.That(MigrateContentPickerValues.ToKeyJson(value, IdToKey), Is.EqualTo(Expected));

[TestCase("\"dcf18a51-6919-4cf8-89d1-36b94ce4d963\"")] // already v17
[TestCase("\"umb://media/dcf18a5169194cf889d136b94ce4d963\"")] // not a document
[TestCase("9999")] // id that no longer exists
[TestCase("not a picker value")]
[TestCase("")]
public void Leaves_everything_else_alone(string value)
=> Assert.That(MigrateContentPickerValues.ToKeyJson(value, IdToKey), Is.Null);
}
}
17 changes: 3 additions & 14 deletions src/uSupport/Client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/uSupport/Client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
"cross-env": "^10.1.0",
"typescript": "~5.9.3",
"vite": "^7.2.4"
},
"overrides": {
"dompurify": "^3.4.17",
"js-yaml": "^4.3.2"
}
}
1 change: 1 addition & 0 deletions src/uSupport/Client/public/umbraco-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "../../../uSupport.Web/umbraco-package-schema.json",
"name": "uSupport",
"alias": "uSupport",
"version": "17.0.0",
"extensions": [
{
"type": "backofficeEntryPoint",
Expand Down
6 changes: 3 additions & 3 deletions src/uSupport/Client/src/api/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { DeleteUsupportTicketByIdData, DeleteUsupportTicketByIdResponses, DeleteUsupportTicketStatusByIdData, DeleteUsupportTicketStatusByIdErrors, DeleteUsupportTicketStatusByIdResponses, DeleteUsupportTicketTypeByIdData, DeleteUsupportTicketTypeByIdErrors, DeleteUsupportTicketTypeByIdResponses, GetUsupportSettingsData, GetUsupportSettingsResponses, GetUsupportTicketActiveData, GetUsupportTicketActiveResponses, GetUsupportTicketAnyResolvedData, GetUsupportTicketAnyResolvedResponses, GetUsupportTicketByIdData, GetUsupportTicketByIdErrors, GetUsupportTicketByIdHistoryData, GetUsupportTicketByIdHistoryResponses, GetUsupportTicketByIdResponses, GetUsupportTicketCommentTicketByTicketIdData, GetUsupportTicketCommentTicketByTicketIdResponses, GetUsupportTicketResolvedData, GetUsupportTicketResolvedResponses, GetUsupportTicketStatusByIdData, GetUsupportTicketStatusByIdDependentTicketsData, GetUsupportTicketStatusByIdDependentTicketsResponses, GetUsupportTicketStatusByIdErrors, GetUsupportTicketStatusByIdResponses, GetUsupportTicketStatusData, GetUsupportTicketStatusResponses, GetUsupportTicketTypeByIdData, GetUsupportTicketTypeByIdDependentTicketsData, GetUsupportTicketTypeByIdDependentTicketsResponses, GetUsupportTicketTypeByIdErrors, GetUsupportTicketTypeByIdResponses, GetUsupportTicketTypeData, GetUsupportTicketTypeResponses, PostUsupportTicketCommentData, PostUsupportTicketCommentErrors, PostUsupportTicketCommentResponses, PostUsupportTicketData, PostUsupportTicketResponses, PostUsupportTicketStatusData, PostUsupportTicketStatusResponses, PostUsupportTicketTypeData, PostUsupportTicketTypeResponses, PutUsupportSettingsData, PutUsupportSettingsResponses, PutUsupportTicketByIdData, PutUsupportTicketByIdErrors, PutUsupportTicketByIdResponses, PutUsupportTicketStatusByIdData, PutUsupportTicketStatusByIdErrors, PutUsupportTicketStatusByIdResponses, PutUsupportTicketStatusSortData, PutUsupportTicketStatusSortResponses, PutUsupportTicketTypeByIdData, PutUsupportTicketTypeByIdErrors, PutUsupportTicketTypeByIdResponses, PutUsupportTicketTypeSortData, PutUsupportTicketTypeSortResponses } from './types.gen';
import type { DeleteUsupportTicketByIdData, DeleteUsupportTicketByIdErrors, DeleteUsupportTicketByIdResponses, DeleteUsupportTicketStatusByIdData, DeleteUsupportTicketStatusByIdErrors, DeleteUsupportTicketStatusByIdResponses, DeleteUsupportTicketTypeByIdData, DeleteUsupportTicketTypeByIdErrors, DeleteUsupportTicketTypeByIdResponses, GetUsupportSettingsData, GetUsupportSettingsResponses, GetUsupportTicketActiveData, GetUsupportTicketActiveResponses, GetUsupportTicketAnyResolvedData, GetUsupportTicketAnyResolvedResponses, GetUsupportTicketByIdData, GetUsupportTicketByIdErrors, GetUsupportTicketByIdHistoryData, GetUsupportTicketByIdHistoryResponses, GetUsupportTicketByIdResponses, GetUsupportTicketCommentTicketByTicketIdData, GetUsupportTicketCommentTicketByTicketIdResponses, GetUsupportTicketResolvedData, GetUsupportTicketResolvedResponses, GetUsupportTicketStatusByIdData, GetUsupportTicketStatusByIdDependentTicketsData, GetUsupportTicketStatusByIdDependentTicketsResponses, GetUsupportTicketStatusByIdErrors, GetUsupportTicketStatusByIdResponses, GetUsupportTicketStatusData, GetUsupportTicketStatusResponses, GetUsupportTicketTypeByIdData, GetUsupportTicketTypeByIdDependentTicketsData, GetUsupportTicketTypeByIdDependentTicketsResponses, GetUsupportTicketTypeByIdErrors, GetUsupportTicketTypeByIdResponses, GetUsupportTicketTypeData, GetUsupportTicketTypeResponses, PostUsupportTicketCommentData, PostUsupportTicketCommentErrors, PostUsupportTicketCommentResponses, PostUsupportTicketData, PostUsupportTicketResponses, PostUsupportTicketStatusData, PostUsupportTicketStatusResponses, PostUsupportTicketTypeData, PostUsupportTicketTypeErrors, PostUsupportTicketTypeResponses, PutUsupportSettingsData, PutUsupportSettingsResponses, PutUsupportTicketByIdData, PutUsupportTicketByIdErrors, PutUsupportTicketByIdResponses, PutUsupportTicketStatusByIdData, PutUsupportTicketStatusByIdErrors, PutUsupportTicketStatusByIdResponses, PutUsupportTicketStatusSortData, PutUsupportTicketStatusSortResponses, PutUsupportTicketTypeByIdData, PutUsupportTicketTypeByIdErrors, PutUsupportTicketTypeByIdResponses, PutUsupportTicketTypeSortData, PutUsupportTicketTypeSortResponses } from './types.gen';

export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
Expand Down Expand Up @@ -130,7 +130,7 @@ export class TicketTypeService {
}

public static postUsupportTicketType<ThrowOnError extends boolean = false>(options?: Options<PostUsupportTicketTypeData, ThrowOnError>) {
return (options?.client ?? client).post<PostUsupportTicketTypeResponses, unknown, ThrowOnError>({
return (options?.client ?? client).post<PostUsupportTicketTypeResponses, PostUsupportTicketTypeErrors, ThrowOnError>({
url: '/umbraco/management/api/v1/usupport/ticket-type',
...options,
headers: {
Expand Down Expand Up @@ -197,7 +197,7 @@ export class TicketService {
}

public static deleteUsupportTicketById<ThrowOnError extends boolean = false>(options: Options<DeleteUsupportTicketByIdData, ThrowOnError>) {
return (options.client ?? client).delete<DeleteUsupportTicketByIdResponses, unknown, ThrowOnError>({
return (options.client ?? client).delete<DeleteUsupportTicketByIdResponses, DeleteUsupportTicketByIdErrors, ThrowOnError>({
url: '/umbraco/management/api/v1/usupport/ticket/{id}',
...options
});
Expand Down
40 changes: 38 additions & 2 deletions src/uSupport/Client/src/api/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ export type CreateTicketRequestModel = {

export type PreviewDeleteItemDtoModel = {
id: string;
ticketId?: string | null;
name?: string | null;
type?: string | null;
path?: string | null;
};

export type ProblemDetails = {
type?: string | null;
title?: string | null;
status?: number | null;
detail?: string | null;
instance?: string | null;
[key: string]: unknown | string | null | string | null | number | null | string | null | string | null | undefined;
};

export type UpdateTicketRequestModel = {
typeId: string;
statusId: string;
Expand Down Expand Up @@ -265,13 +275,15 @@ export type DeleteUsupportTicketStatusByIdErrors = {
/**
* Bad Request
*/
400: unknown;
400: ProblemDetails;
/**
* Not Found
*/
404: unknown;
};

export type DeleteUsupportTicketStatusByIdError = DeleteUsupportTicketStatusByIdErrors[keyof DeleteUsupportTicketStatusByIdErrors];

export type DeleteUsupportTicketStatusByIdResponses = {
/**
* OK
Expand Down Expand Up @@ -384,6 +396,15 @@ export type PostUsupportTicketTypeData = {
url: '/umbraco/management/api/v1/usupport/ticket-type';
};

export type PostUsupportTicketTypeErrors = {
/**
* Bad Request
*/
400: ProblemDetails;
};

export type PostUsupportTicketTypeError = PostUsupportTicketTypeErrors[keyof PostUsupportTicketTypeErrors];

export type PostUsupportTicketTypeResponses = {
/**
* OK
Expand All @@ -406,13 +427,15 @@ export type DeleteUsupportTicketTypeByIdErrors = {
/**
* Bad Request
*/
400: unknown;
400: ProblemDetails;
/**
* Not Found
*/
404: unknown;
};

export type DeleteUsupportTicketTypeByIdError = DeleteUsupportTicketTypeByIdErrors[keyof DeleteUsupportTicketTypeByIdErrors];

export type DeleteUsupportTicketTypeByIdResponses = {
/**
* OK
Expand Down Expand Up @@ -455,12 +478,18 @@ export type PutUsupportTicketTypeByIdData = {
};

export type PutUsupportTicketTypeByIdErrors = {
/**
* Bad Request
*/
400: ProblemDetails;
/**
* Not Found
*/
404: unknown;
};

export type PutUsupportTicketTypeByIdError = PutUsupportTicketTypeByIdErrors[keyof PutUsupportTicketTypeByIdErrors];

export type PutUsupportTicketTypeByIdResponses = {
/**
* OK
Expand Down Expand Up @@ -527,6 +556,13 @@ export type DeleteUsupportTicketByIdData = {
url: '/umbraco/management/api/v1/usupport/ticket/{id}';
};

export type DeleteUsupportTicketByIdErrors = {
/**
* Not Found
*/
404: unknown;
};

export type DeleteUsupportTicketByIdResponses = {
/**
* OK
Expand Down
2 changes: 1 addition & 1 deletion src/uSupport/Client/src/dashboards/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const manifests: Array<UmbExtensionManifest> = [
alias: 'uSupport.Dashboard.User',
name: 'uSupport User Dashboard',
element: () => import('./user-dashboard.element.js'),
weight: -10,
weight: 1000,
meta: { label: '#dashboardTabs_uSupportUserDashboard', pathname: 'support' },
conditions: [{ alias: 'Umb.Condition.SectionAlias', match: 'Umb.Section.Content' }],
},
Expand Down
1 change: 0 additions & 1 deletion src/uSupport/Client/src/localization/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export default {
details: '詳細',
internalComment: '内部コメント',
internalCommentDescription: 'uSupport ユーザーにのみ表示されます',
internalCommentUpdated: '内部コメントを更新しました',
emailTo: 'メール送信先:',

comment: 'コメント',
Expand Down
4 changes: 2 additions & 2 deletions src/uSupport/Client/src/shared/lookup-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class DeleteLookupAction extends LookupActionBase {
<ul>
${dependents.map(
(ticket) =>
html`<li><a href=${`/umbraco/${workspacePath(USUPPORT_TICKET_ENTITY_TYPE, `edit/${ticket.id}`)}`}>${ticket.name}</a></li>`,
html`<li><a href=${`/umbraco/${workspacePath(USUPPORT_TICKET_ENTITY_TYPE, `edit/${ticket.id}`)}`}>${ticket.ticketId} - ${ticket.name}</a></li>`,
)}
</ul>
`,
Expand All @@ -70,7 +70,7 @@ export class DeleteLookupAction extends LookupActionBase {

await umbConfirmModal(this, {
headline: this.localize.term('actions_delete'),
content: this.localize.term('defaultdialogs_confirmdelete') + '?',
content: this.localize.term('defaultdialogs_confirmdelete'),
color: 'danger',
confirmLabel: this.localize.term('actions_delete'),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ export class USupportTicketTypeEditorElement extends UmbLitElement {
@change=${(e: Event) =>
this.#update({ dataTypeKey: (e.target as HTMLInputElement).value || null })}></umb-data-type-input>
</umb-property-layout>
<umb-property-layout label=${this.localize.term('uSupport_propertyName')}>
<umb-property-layout label=${this.localize.term('uSupport_propertyName')} ?mandatory=${!!model.dataTypeKey}>
<uui-input
slot="editor"
label=${this.localize.term('uSupport_propertyName')}
?required=${!!model.dataTypeKey}
.value=${model.propertyName ?? ''}
@input=${this.#text('propertyName')}></uui-input>
</umb-property-layout>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
import { sanitizeHTML } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import { USupportRepository, type TicketComment } from '../../repository/usupport.repository.js';
Expand Down Expand Up @@ -59,7 +60,7 @@ export class USupportCommentsElement extends UmbLitElement {
<div>
<strong>${comment.user?.name}</strong>
<small>${formatDate(comment.date)}</small>
<div class="body">${unsafeHTML(comment.comment)}</div>
<div class="body">${unsafeHTML(sanitizeHTML(comment.comment ?? ''))}</div>
</div>
</div>
`,
Expand Down
Loading
Loading