Skip to content

Commit 68e95f6

Browse files
committed
refactor: simplify frontend architecture (WI-1239-WI-1245)
1 parent e175c8c commit 68e95f6

79 files changed

Lines changed: 3792 additions & 4086 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/openapi-v2.json

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20508,6 +20508,14 @@
2050820508
},
2050920509
"type": "array"
2051020510
},
20511+
"description": {
20512+
"description": "The description value.",
20513+
"type": "string"
20514+
},
20515+
"field_type": {
20516+
"description": "The field type value.",
20517+
"type": "string"
20518+
},
2051120519
"label": {
2051220520
"description": "The label value.",
2051320521
"type": "string"
@@ -20528,35 +20536,17 @@
2052820536
"description": "The value help value.",
2052920537
"nullable": true,
2053020538
"properties": {
20531-
"api_version": {
20532-
"description": "The api version value.",
20539+
"source": {
20540+
"description": "The source value.",
2053320541
"type": "string"
2053420542
},
20535-
"endpoint": {
20536-
"description": "The endpoint value.",
20537-
"type": "string"
20538-
},
20539-
"label_fields": {
20540-
"description": "The label fields value.",
20541-
"items": {
20542-
"type": "string"
20543-
},
20544-
"type": "array"
20545-
},
20546-
"paginated": {
20547-
"description": "The paginated value.",
20548-
"type": "boolean"
20549-
},
2055020543
"value_field": {
2055120544
"description": "The value field value.",
2055220545
"type": "string"
2055320546
}
2055420547
},
2055520548
"required": [
20556-
"api_version",
20557-
"endpoint",
20558-
"label_fields",
20559-
"paginated",
20549+
"source",
2056020550
"value_field"
2056120551
],
2056220552
"type": "object"
@@ -20570,6 +20560,10 @@
2057020560
"items": {
2057120561
"additionalProperties": false,
2057220562
"properties": {
20563+
"description": {
20564+
"description": "The description value.",
20565+
"type": "string"
20566+
},
2057320567
"label": {
2057420568
"description": "The label value.",
2057520569
"type": "string"
@@ -20618,6 +20612,39 @@
2061820612
],
2061920613
"type": "object"
2062020614
},
20615+
"GetQueryLanguageValuesResult": {
20616+
"additionalProperties": false,
20617+
"properties": {
20618+
"data": {
20619+
"items": {
20620+
"additionalProperties": false,
20621+
"properties": {
20622+
"description": {
20623+
"description": "The description value.",
20624+
"type": "string"
20625+
},
20626+
"label": {
20627+
"description": "The label value.",
20628+
"type": "string"
20629+
},
20630+
"value": {
20631+
"description": "The value value."
20632+
}
20633+
},
20634+
"required": [
20635+
"label",
20636+
"value"
20637+
],
20638+
"type": "object"
20639+
},
20640+
"type": "array"
20641+
}
20642+
},
20643+
"required": [
20644+
"data"
20645+
],
20646+
"type": "object"
20647+
},
2062120648
"GetStatusCategoriesCategoryIdResult": {
2062220649
"additionalProperties": false,
2062320650
"properties": {
@@ -77791,6 +77818,109 @@
7779177818
]
7779277819
}
7779377820
},
77821+
"/query-language/values": {
77822+
"get": {
77823+
"description": "Search query language completion values. Results include only resources visible to the authenticated caller; collections use their documented stable order and bounds.",
77824+
"operationId": "getQueryLanguageValues",
77825+
"parameters": [
77826+
{
77827+
"description": "Completion value catalog to search.",
77828+
"in": "query",
77829+
"name": "source",
77830+
"required": true,
77831+
"schema": {
77832+
"enum": [
77833+
"workspaces",
77834+
"statuses",
77835+
"status_categories",
77836+
"priorities",
77837+
"users",
77838+
"milestones",
77839+
"iterations",
77840+
"projects",
77841+
"item_types",
77842+
"labels"
77843+
],
77844+
"type": "string"
77845+
}
77846+
},
77847+
{
77848+
"description": "Resource field returned as the query-language value.",
77849+
"in": "query",
77850+
"name": "value_field",
77851+
"required": true,
77852+
"schema": {
77853+
"enum": [
77854+
"id",
77855+
"name",
77856+
"key"
77857+
],
77858+
"type": "string"
77859+
}
77860+
},
77861+
{
77862+
"description": "Case-insensitive value-label search text.",
77863+
"in": "query",
77864+
"name": "q",
77865+
"required": false,
77866+
"schema": {
77867+
"type": "string"
77868+
}
77869+
},
77870+
{
77871+
"description": "Maximum number of completion values to return.",
77872+
"in": "query",
77873+
"name": "limit",
77874+
"required": false,
77875+
"schema": {
77876+
"default": 50,
77877+
"maximum": 100,
77878+
"minimum": 1,
77879+
"type": "integer"
77880+
}
77881+
}
77882+
],
77883+
"responses": {
77884+
"200": {
77885+
"content": {
77886+
"application/json": {
77887+
"schema": {
77888+
"$ref": "#/components/schemas/GetQueryLanguageValuesResult"
77889+
}
77890+
}
77891+
},
77892+
"description": "Search query language completion values response."
77893+
},
77894+
"400": {
77895+
"$ref": "#/components/responses/InvalidRequestError"
77896+
},
77897+
"401": {
77898+
"$ref": "#/components/responses/AuthenticationError"
77899+
},
77900+
"403": {
77901+
"$ref": "#/components/responses/PermissionError"
77902+
},
77903+
"429": {
77904+
"$ref": "#/components/responses/RateLimitError"
77905+
},
77906+
"500": {
77907+
"$ref": "#/components/responses/InternalError"
77908+
}
77909+
},
77910+
"security": [
77911+
{
77912+
"BearerAuth": []
77913+
}
77914+
],
77915+
"summary": "Search query language completion values",
77916+
"tags": [
77917+
"Work items"
77918+
],
77919+
"x-required-scopes": [
77920+
"items:read"
77921+
]
77922+
}
77923+
},
7779477924
"/recurrence-rules/preview": {
7779577925
"post": {
7779677926
"description": "Preview recurrence rule. The server validates resource ownership and command preconditions before persisting changes. Unknown JSON fields are rejected when the operation accepts a body.",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function clickOutside(node) {
2+
function handleClick(event) {
3+
if (!node.contains(event.target)) {
4+
node.dispatchEvent(new CustomEvent('clickOutside'));
5+
}
6+
}
7+
8+
document.addEventListener('click', handleClick, true);
9+
return {
10+
destroy() {
11+
document.removeEventListener('click', handleClick, true);
12+
},
13+
};
14+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Keep admin settings out of the initial admin bundle. Each tab is loaded only
2+
// when its route is active.
3+
export const ADMIN_COMPONENT_LOADERS = {
4+
'custom-fields': () => import('../settings/CustomFields.svelte'),
5+
workspaces: () => import('../workspaces/Workspaces.svelte'),
6+
screens: () => import('../pages/Screens.svelte'),
7+
statuses: () => import('../features/workflows/StatusContainer.svelte'),
8+
workflows: () => import('../features/workflows/WorkflowBuilder.svelte'),
9+
'configuration-sets': () => import('../settings/ConfigurationSetManager.svelte'),
10+
'condition-sets': () => import('../settings/ConditionSetManager.svelte'),
11+
'approval-sets': () => import('../settings/ApprovalSetManager.svelte'),
12+
'notification-settings': () => import('../settings/NotificationSettings.svelte'),
13+
'email-templates': () => import('../settings/EmailTemplateManager.svelte'),
14+
channels: () => import('../features/channels/Channels.svelte'),
15+
'link-types': () => import('../settings/LinkTypeManager.svelte'),
16+
users: () => import('../settings/UserManager.svelte'),
17+
groups: () => import('../settings/GroupManager.svelte'),
18+
permissions: () => import('../layout/PermissionsContainer.svelte'),
19+
'workspace-roles': () => import('../settings/RoleManager.svelte'),
20+
attachments: () => import('../settings/AttachmentSettings.svelte'),
21+
modules: () => import('../settings/ModuleSettings.svelte'),
22+
themes: () => import('../settings/ThemeManager.svelte'),
23+
'hierarchy-levels': () => import('../settings/HierarchyLevelManager.svelte'),
24+
'item-types': () => import('../settings/ItemTypeManager.svelte'),
25+
priorities: () => import('../settings/PriorityManager.svelte'),
26+
sso: () => import('../settings/SSOContainer.svelte'),
27+
'scm-providers': () => import('../settings/SCMProviderManager.svelte'),
28+
'integration-providers': () => import('../settings/IntegrationsManager.svelte'),
29+
'llm-connections': () => import('../settings/AIContainer.svelte'),
30+
'action-capabilities': () => import('../settings/ActionCapabilitiesManager.svelte'),
31+
'system-import': () => import('../jira-import/SystemImportPage.svelte'),
32+
security: () => import('../settings/SecuritySettings.svelte'),
33+
assets: () => import('../features/assets/AssetManager.svelte'),
34+
diagnostics: () => import('../settings/Diagnostics.svelte'),
35+
'permission-set-detail': () => import('../settings/PermissionSetEdit.svelte'),
36+
'configuration-set-detail': () => import('../settings/ConfigurationSetDetail.svelte'),
37+
'condition-set-detail': () => import('../settings/ConditionSetDetail.svelte'),
38+
'approval-set-detail': () => import('../settings/ApprovalSetDetail.svelte'),
39+
'form-channel': () => import('../features/channels/FormChannelPage.svelte'),
40+
'portal-channel': () => import('../features/channels/PortalChannelPage.svelte'),
41+
};
42+
43+
export function getAdminComponentProps(componentKey, loadedExtensions) {
44+
if (componentKey === 'workspaces') return { noPadding: true };
45+
if (componentKey === 'channels') return { embedded: true };
46+
if (componentKey === 'sso') return { extensions: loadedExtensions };
47+
return {};
48+
}

frontend/src/lib/api/milestones.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ function planningQuery(filters = {}) {
1717
return params.toString();
1818
}
1919

20-
async function listPlanning(path, filters = {}) {
20+
async function listPlanning(path, filters = {}, requestOptions = {}) {
2121
const query = planningQuery(filters);
2222
const globalPath = `${path}${query ? `?${query}` : ''}`;
23-
if (filters.workspace_id == null) return fetchAllV2Pages(globalPath);
23+
if (filters.workspace_id == null) return fetchAllV2Pages(globalPath, requestOptions);
2424
const workspacePath = `/workspaces/${filters.workspace_id}${path}${query ? `?${query}` : ''}`;
25-
const local = fetchAllV2Pages(workspacePath);
25+
const local = fetchAllV2Pages(workspacePath, requestOptions);
2626
if (filters.include_global === false) return local;
27-
const [localRows, globalRows] = await Promise.all([local, fetchAllV2Pages(globalPath)]);
27+
const [localRows, globalRows] = await Promise.all([
28+
local,
29+
fetchAllV2Pages(globalPath, requestOptions),
30+
]);
2831
return [...localRows, ...globalRows];
2932
}
3033

@@ -45,7 +48,8 @@ function iterationPatch(data) {
4548
}
4649

4750
export const milestones = {
48-
getAll: (filters = {}) => listPlanning('/milestones', filters),
51+
getAll: (filters = {}, requestOptions = {}) =>
52+
listPlanning('/milestones', filters, requestOptions),
4953
get: (id) => fetchV2Data(`/milestones/${id}`),
5054
create: (data) => planningCreate('/milestones', data),
5155
update: (id, data) =>
@@ -89,7 +93,8 @@ export const milestones = {
8993
export const iterationTypes = createCrudClient('/iteration-types');
9094

9195
export const iterations = {
92-
getAll: (filters = {}) => listPlanning('/iterations', filters),
96+
getAll: (filters = {}, requestOptions = {}) =>
97+
listPlanning('/iterations', filters, requestOptions),
9398
get: (id) => fetchV2Data(`/iterations/${id}`),
9499
create: (data) => planningCreate('/iterations', data),
95100
update: (id, data) =>
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { fetchAllV2Pages, fetchAPI, fetchV2Data } from './core.js';
1+
import { fetchV2Data } from './core.js';
22

33
export const queryLanguage = {
44
getCatalog: () => fetchV2Data('/query-language/catalog'),
55

6-
async getValues(valueHelp) {
7-
if (!valueHelp?.endpoint) return [];
6+
async getValues(valueHelp, query = '') {
7+
if (!valueHelp?.source || !valueHelp?.value_field) return [];
88

9-
if (valueHelp.api_version === 'v1') {
10-
return (await fetchAPI(valueHelp.endpoint)) || [];
11-
}
12-
if (valueHelp.paginated) {
13-
return fetchAllV2Pages(valueHelp.endpoint);
14-
}
15-
return (await fetchV2Data(valueHelp.endpoint)) || [];
9+
const params = new URLSearchParams({
10+
source: valueHelp.source,
11+
value_field: valueHelp.value_field,
12+
});
13+
if (query) params.set('q', query);
14+
return (await fetchV2Data(`/query-language/values?${params.toString()}`)) || [];
1615
},
1716
};

frontend/src/lib/api/workflows.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fetchV2Data } from './core.js';
1+
import { fetchAPI, fetchV2Data } from './core.js';
22
import { createCrudClient } from './createCrudClient.js';
33

44
export const statusCategories = createCrudClient('/status-categories', { v2: true });
@@ -56,10 +56,7 @@ async function getWorkflow(id, requestOptions = {}) {
5656
}
5757

5858
async function getAllWithTransitions() {
59-
const items = (await workflowCRUD.getAll()) ?? [];
60-
return Promise.all(
61-
items.map(async (workflow) => ({ ...workflow, transitions: await getTransitions(workflow.id) }))
62-
);
59+
return fetchAPI('/workflows?include_transitions=true');
6360
}
6461

6562
export const workflows = {

frontend/src/lib/components/Button.svelte

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,18 @@
3030
export { className as class };
3131
3232
const buttonType = $derived(/** @type {"button"|"submit"|"reset"} */ (type));
33+
const effectiveDisabled = $derived(disabled || loading);
3334
3435
let buttonEl = $state(null);
3536
37+
function handleClick(event) {
38+
if (effectiveDisabled) {
39+
event.preventDefault();
40+
return;
41+
}
42+
onclick?.(event);
43+
}
44+
3645
// Install/uninstall @github/hotkey on the button element
3746
$effect(() => {
3847
if (!buttonEl || !hotkeyConfig?.key) return;
@@ -155,11 +164,11 @@
155164
<!-- Snippet for the button/link element -->
156165
{#snippet buttonElement()}
157166
{#if href}
158-
<a bind:this={buttonEl} {id} {href} {target} {style} data-testid={dataTestid} data-page-id={dataPageId} class={allClasses} onclick={(e) => onclick?.(e)}>
167+
<a bind:this={buttonEl} {id} {href} {target} {style} aria-disabled={effectiveDisabled} data-testid={dataTestid} data-page-id={dataPageId} class={allClasses} onclick={handleClick}>
159168
{@render linkContent()}
160169
</a>
161170
{:else}
162-
<button bind:this={buttonEl} {id} type={buttonType} {disabled} {style} data-testid={dataTestid} data-page-id={dataPageId} class={allClasses} onclick={(e) => onclick?.(e)}>
171+
<button bind:this={buttonEl} {id} type={buttonType} disabled={effectiveDisabled} aria-busy={loading} {style} data-testid={dataTestid} data-page-id={dataPageId} class={allClasses} onclick={handleClick}>
163172
{@render buttonContent()}
164173
</button>
165174
{/if}

0 commit comments

Comments
 (0)