Skip to content

Commit 54ce6fb

Browse files
committed
feat(task): add baseMixin support in TaskTypes
- Add baseMixin field to TaskType interface (plugins/task/src/index.ts) - Add @prop decorator for baseMixin in model (models/task/src/index.ts) - Modify mixin creation logic to use baseMixin when provided (plugins/task/src/utils.ts) - Add validateMixinHierarchy function for inheritance validation - Create MixinSelector.svelte component for UI selection - Add i18n strings for BaseMixin, BaseMixinDescription, NoBaseMixin This allows TaskTypes to inherit attributes from existing mixins while maintaining backwards compatibility. Signed-off-by: Luis Rosas <luisrosasx@users.noreply.github.com>
1 parent baacc66 commit 54ce6fb

6 files changed

Lines changed: 114 additions & 2 deletions

File tree

models/task/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ export class TTaskType extends TDoc implements TaskType {
210210
@Prop(ArrOf(TypeRef(task.class.TaskType)), getEmbeddedLabel('Parent'))
211211
allowedAsChildOf!: Ref<TaskType>[] // In case of specified, task type is for sub-tasks
212212

213+
@Prop(TypeRef(core.class.Mixin), getEmbeddedLabel('Base Mixin'))
214+
baseMixin?: Ref<Mixin<Task>> // Existing mixin to inherit attributes from
215+
213216
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('Task class'))
214217
ofClass!: Ref<Class<Task>> // Base class for task
215218

plugins/task-assets/lang/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
"StatusChange": "Status changed",
8080
"TaskCreated": "Task created",
8181
"TaskType": "Task type",
82+
"BaseMixin": "Base Mixin",
83+
"BaseMixinDescription": "Inherit attributes from an existing mixin",
84+
"NoBaseMixin": "None (create new)",
8285
"ManageProjects": "Project types",
8386
"Export": "Export",
8487
"CreateProjectType": "Create project type",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script lang="ts">
2+
import { Class, Mixin, Ref } from '@hcengineering/core'
3+
import { Task } from '@hcengineering/task'
4+
import { ButtonMenu, Label } from '@hcengineering/ui'
5+
import { createEventDispatcher } from 'svelte'
6+
import { getClient } from '@hcengineering/presentation'
7+
import plugin from '../../plugin'
8+
9+
export let baseClass: Ref<Class<Task>>
10+
export let value: Ref<Mixin<Task>> | undefined = undefined
11+
export let readonly = false
12+
export let buttonKind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
13+
export let buttonSize: 'large' | 'medium' | 'small' = 'large'
14+
15+
const dispatch = createEventDispatcher()
16+
const client = getClient()
17+
const hierarchy = client.getHierarchy()
18+
19+
// Find all mixins that derive from baseClass
20+
function getAvailableMixins (): Array<{ id: string; label: string }> {
21+
const result: Array<{ id: string; label: string }> = [
22+
{ id: '', label: 'None (create new)' }
23+
]
24+
25+
try {
26+
const descendants = hierarchy.getDescendants(baseClass)
27+
for (const descendant of descendants) {
28+
const cls = hierarchy.getClass(descendant)
29+
if (cls.kind === 'mixin' && descendant !== baseClass) {
30+
result.push({
31+
id: descendant as string,
32+
label: cls.label ?? descendant
33+
})
34+
}
35+
}
36+
} catch (e) {
37+
console.error('Error getting available mixins:', e)
38+
}
39+
40+
return result
41+
}
42+
43+
function handleSelect (evt: CustomEvent<string>): void {
44+
if (evt.detail !== undefined) {
45+
value = evt.detail === '' ? undefined : (evt.detail as Ref<Mixin<Task>>)
46+
dispatch('change', value)
47+
}
48+
}
49+
50+
$: items = getAvailableMixins()
51+
$: selected = items.find((it) => it.id === (value ?? ''))
52+
</script>
53+
54+
<div class="mixin-selector">
55+
<Label label={plugin.string.BaseMixin} />
56+
{#if readonly}
57+
{#if selected}
58+
<span class="selected-value">{selected.label}</span>
59+
{/if}
60+
{:else}
61+
<ButtonMenu
62+
selected={value ?? ''}
63+
{items}
64+
label={selected?.label ?? plugin.string.NoBaseMixin}
65+
kind={buttonKind}
66+
size={buttonSize}
67+
on:selected={handleSelect}
68+
/>
69+
{/if}
70+
</div>
71+
72+
<style lang="scss">
73+
.mixin-selector {
74+
display: flex;
75+
flex-direction: column;
76+
gap: 0.5rem;
77+
}
78+
79+
.selected-value {
80+
color: var(--theme-content-color);
81+
}
82+
</style>

plugins/task-resources/src/plugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ export default mergeIds(taskId, task, {
8989
RenameStatus: '' as IntlString,
9090
UpdateTasksStatusRequest: '' as IntlString,
9191
TaskTypes: '' as IntlString,
92-
Collections: '' as IntlString
92+
Collections: '' as IntlString,
93+
BaseMixin: '' as IntlString,
94+
BaseMixinDescription: '' as IntlString,
95+
NoBaseMixin: '' as IntlString
9396
},
9497
status: {
9598
AssigneeRequired: '' as IntlString

plugins/task/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export interface TaskType extends Doc, IconProps {
120120
// Specify if task is allowed to be used as subtask of following tasks.
121121
allowedAsChildOf?: Ref<TaskType>[]
122122

123+
// Existing mixin to inherit attributes from (optional)
124+
baseMixin?: Ref<Mixin<Task>>
125+
123126
ofClass: Ref<Class<Task>> // Base class for task
124127
targetClass: Ref<Class<Task>> // Class or Mixin mixin to hold all user defined attributes.
125128

plugins/task/src/utils.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import core, {
2121
DocumentQuery,
2222
Hierarchy,
2323
IdMap,
24+
Mixin,
2425
Ref,
2526
Status,
2627
TxOperations,
@@ -330,11 +331,14 @@ async function createTaskTypes (
330331
const targetClassId = `${taskId}:type:mixin` as Ref<Class<Task>>
331332
tdata.targetClass = targetClassId
332333

334+
// Use baseMixin if provided, otherwise extend ofClass directly
335+
const extendsClass = data.baseMixin ?? data.ofClass
336+
333337
await client.createDoc(
334338
core.class.Mixin,
335339
core.space.Model,
336340
{
337-
extends: data.ofClass,
341+
extends: extendsClass,
338342
kind: ClassifierKind.MIXIN,
339343
label: ofClassClass.label,
340344
icon: ofClassClass.icon
@@ -353,3 +357,17 @@ async function createTaskTypes (
353357
}
354358
return hasUpdates
355359
}
360+
361+
/**
362+
* @public
363+
* Validates that a baseMixin is compatible with the ofClass hierarchy.
364+
* Returns true if baseMixin is undefined or if it derives from ofClass.
365+
*/
366+
export function validateMixinHierarchy (
367+
hierarchy: Hierarchy,
368+
baseMixin: Ref<Mixin<Task>> | undefined,
369+
ofClass: Ref<Class<Task>>
370+
): boolean {
371+
if (baseMixin === undefined) return true
372+
return hierarchy.isDerived(baseMixin, ofClass)
373+
}

0 commit comments

Comments
 (0)