diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f64e0ff7..8739babab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 5.3.0
+
+- Added `multiple` prop to `c-select`, allowing for the selection of multiple models.
+- `c-select-many-to-many` is now based on `c-select` rather than `v-autocomplete`. As a result, it has gained support for all of the props and slots of `c-select`.
+
# 5.2.1
## Fixes
diff --git a/docs/stacks/vue/coalesce-vue-vuetify/components/c-input.md b/docs/stacks/vue/coalesce-vue-vuetify/components/c-input.md
index 627766487..00cb2f9bb 100644
--- a/docs/stacks/vue/coalesce-vue-vuetify/components/c-input.md
+++ b/docs/stacks/vue/coalesce-vue-vuetify/components/c-input.md
@@ -20,6 +20,7 @@ A summary of the components delegated to, by type:
- date: [c-datetime-picker](/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.md)
- model: [c-select](/stacks/vue/coalesce-vue-vuetify/components/c-select.md)
- [[ManyToMany]](/modeling/model-components/attributes/many-to-many.md) collection: [c-select-many-to-many](/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md)
+- Model collection (not many-to-many): [c-select](/stacks/vue/coalesce-vue-vuetify/components/c-select.md)
- Non-object collection: [c-select-values](/stacks/vue/coalesce-vue-vuetify/components/c-select-values.md)
Any other unsupported type will simply be displayed with [c-display](/stacks/vue/coalesce-vue-vuetify/components/c-display.md), unless a [default slot](https://vuejs.org/guide/components/slots.html) is provided - in that case, the default slot will be rendered instead.
diff --git a/docs/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md b/docs/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md
index fc50426c1..59f12072d 100644
--- a/docs/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md
+++ b/docs/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md
@@ -19,50 +19,17 @@ It is unlikely that you'll ever need to use this component directly - it is high
``` vue-html
-```
-
-``` vue-html
-
```
## Props
-
-
-A metadata specifier for the value being bound. One of:
-
-- A string with the name of the value belonging to `model`.
-- A direct reference to a metadata object.
-- A string in dot-notation that starts with a type name.
-
-::: tip Note
-c-select-many-to-many expects metadata for the "real" collection navigation property on a model. If you provide it the string you passed to [[ManyToMany]](/modeling/model-components/attributes/many-to-many.md), an error wil be thrown.
-:::
+See [c-select / Props](./c-select.md#props).
-
-
-An object owning the value that was specified by the `for` prop. If provided, the input will be bound to the corresponding property on the `model` object.
-
-
-
-If binding the component with ``v-model``, accepts the ``value`` part of ``v-model``.
-
-
-
-An optional set of [Data Source Standard Parameters](/modeling/model-components/data-sources.md#standard-parameters) to pass to API calls made to the server.
-
-
-
-If provided and non-false, enables [response caching](/stacks/vue/layers/api-clients.md#response-caching) on the component's internal API caller.
-
+Since `c-select-many-to-many` internally uses `c-select` as its implementation, all props of `c-select` are also supported by `c-select-many-to-many`.
## Events
diff --git a/docs/stacks/vue/coalesce-vue-vuetify/components/c-select.md b/docs/stacks/vue/coalesce-vue-vuetify/components/c-select.md
index 8737e08ab..ec274deb6 100644
--- a/docs/stacks/vue/coalesce-vue-vuetify/components/c-select.md
+++ b/docs/stacks/vue/coalesce-vue-vuetify/components/c-select.md
@@ -29,6 +29,17 @@ Binding an arbitrary primary key value or an arbitrary object:
```
+Multi-select:
+
+``` vue-html
+
+
+
+
+
+
+```
+
Examples of other props:
``` vue-html
@@ -48,6 +59,8 @@ Examples of other props:
## Props
+Note: In addition to the below props, `c-select` also supports most props that are supported by Vuetify's [v-text-field](https://vuetifyjs.com/en/components/text-fields/).
+
A metadata specifier for the value being bound. One of:
@@ -55,10 +68,9 @@ A metadata specifier for the value being bound. One of:
- The name of a foreign key or reference navigation property belonging to `model`.
- The name of a model type.
- A direct reference to a metadata object.
-- A string in dot-notation that starts with a type name that resolves to a foreign key or reference navigation property.
::: tip
-When binding by a key value, if the corresponding object cannot be found (e.g. there is no navigation property, or the navigation property is null), c-select will automatically attempt to load the object from the server so it can be displayed in the UI.
+When the binding can only locate a PK value and the corresponding object cannot be found (e.g. there is no navigation property, or the navigation property is null), c-select will automatically attempt to load the object from the server so it can be displayed in the UI.
:::
@@ -72,13 +84,19 @@ modelValue?: any // Vue 3" lang="ts" />
When binding the component with ``v-model``, accepts the ``value`` part of ``v-model``. If `for` was specified as a foreign key, this will expect a key; likewise, if `for` was specified as a type or as a navigation property, this will expect an object.
-
+
+
+Enables multi-select functionality. Bindings for `modelValue`, `keyValue`, and `objectValue` will accept and emit arrays instead of single values.
+
+
-When bound with `v-model:key-value="keyValue"`, allows binding the primary key of the selected object explicitly.
+When bound with `v-model:key-value="keyValue"`, allows binding the primary key of the selected object explicitly. Binds an array when in multi-select mode.
-
+
-When bound with `v-model:object-value="objectValue"`, allows binding the selected object explicitly.
+When bound with `v-model:object-value="objectValue"`, allows binding the selected object explicitly. Binds an array when in multi-select mode.
@@ -149,8 +167,8 @@ createMethods = {
## Slots
-`#item="{ item, search }"` - Slot used to customize the text of both items inside the list, as well as the text of selected items. By default, items are rendered with [c-display](/stacks/vue/coalesce-vue-vuetify/components/c-display.md). Slot is passed a parameter `item` containing a [model instance](/stacks/vue/layers/models.md), and `search` containing the current search query.
+`#item="{ item: TModel, search: string }"` - Slot used to customize the text of both items inside the list, as well as the text of selected items. By default, items are rendered with [c-display](/stacks/vue/coalesce-vue-vuetify/components/c-display.md). Slot is passed a parameter `item` containing a [model instance](/stacks/vue/layers/models.md), and `search` containing the current search query.
-`#list-item="{ item, search }"` - Slot used to customize the text of items inside the list. If not provided, falls back to the `item` slot.
+`#list-item="{ item: TModel, search: string, selected: boolean }"` - Slot used to customize the text of items inside the list. If not provided, falls back to the `item` slot. Contents are wrapped in a `v-list-item-title`.
-`#selected-item="{ item, search }"` - Slot used to customize the text of selected items. If not provided, falls back to the `item` slot.
\ No newline at end of file
+`#selected-item="{ item: TModel, search: string, index: number, remove: () => void }"` - Slot used to customize the text of selected items. If not provided, falls back to the `item` slot. The `remove` function will deselect the item and is only valid when using multi-select.
\ No newline at end of file
diff --git a/playground/Coalesce.Domain/Case.cs b/playground/Coalesce.Domain/Case.cs
index 79538474c..63d49549a 100644
--- a/playground/Coalesce.Domain/Case.cs
+++ b/playground/Coalesce.Domain/Case.cs
@@ -232,6 +232,15 @@ public override IQueryable GetQuery(IDataSourceParameters parameters) => D
.IncludeChildren();
}
+ public class MissingManyToManyFarSide(CrudContext context) : StandardDataSource(context)
+ {
+ public override IQueryable GetQuery(IDataSourceParameters parameters) => Db.Cases
+ .Include(c => c.CaseProducts)
+ .Include(c => c.AssignedTo)
+ .Include(c => c.ReportedBy);
+ }
+
+
///
/// Returns a list of summary information about Cases
///
diff --git a/playground/Coalesce.Domain/Person.cs b/playground/Coalesce.Domain/Person.cs
index 660c24ed3..e9864493e 100644
--- a/playground/Coalesce.Domain/Person.cs
+++ b/playground/Coalesce.Domain/Person.cs
@@ -298,7 +298,7 @@ public static string[] MethodWithStringArrayParameter(AppDbContext db, string[]
}
[Coalesce, Execute]
- public static Person MethodWithEntityParameter(AppDbContext db, Person person)
+ public static Person MethodWithEntityParameter(AppDbContext db, Person person, Person[] people)
{
return person;
}
diff --git a/playground/Coalesce.Web.Vue2/Api/Generated/PersonController.g.cs b/playground/Coalesce.Web.Vue2/Api/Generated/PersonController.g.cs
index a39ceed94..62fc17e62 100644
--- a/playground/Coalesce.Web.Vue2/Api/Generated/PersonController.g.cs
+++ b/playground/Coalesce.Web.Vue2/Api/Generated/PersonController.g.cs
@@ -839,11 +839,13 @@ [FromBody] MethodWithStringArrayParameterParameters _params
[Authorize]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
public virtual ItemResult MethodWithEntityParameter(
- [FromForm(Name = "person")] PersonParameter person)
+ [FromForm(Name = "person")] PersonParameter person,
+ [FromForm(Name = "people")] PersonParameter[] people)
{
var _params = new
{
- Person = !Request.Form.HasAnyValue(nameof(person)) ? null : person
+ Person = !Request.Form.HasAnyValue(nameof(person)) ? null : person,
+ People = !Request.Form.HasAnyValue(nameof(people)) ? null : people.ToList()
};
if (Context.Options.ValidateAttributesForMethods)
@@ -857,7 +859,8 @@ public virtual ItemResult MethodWithEntityParameter(
var _mappingContext = new MappingContext(Context);
var _methodResult = Coalesce.Domain.Person.MethodWithEntityParameter(
Db,
- _params.Person?.MapToNew(_mappingContext)
+ _params.Person?.MapToNew(_mappingContext),
+ _params.People?.Select(_m => _m.MapToNew(_mappingContext)).ToArray()
);
var _result = new ItemResult();
_result.Object = Mapper.MapToDto(_methodResult, _mappingContext, includeTree);
@@ -867,6 +870,7 @@ public virtual ItemResult MethodWithEntityParameter(
public class MethodWithEntityParameterParameters
{
public PersonParameter Person { get; set; }
+ public PersonParameter[] People { get; set; }
}
///
@@ -890,7 +894,8 @@ [FromBody] MethodWithEntityParameterParameters _params
var _mappingContext = new MappingContext(Context);
var _methodResult = Coalesce.Domain.Person.MethodWithEntityParameter(
Db,
- _params.Person?.MapToNew(_mappingContext)
+ _params.Person?.MapToNew(_mappingContext),
+ _params.People?.Select(_m => _m.MapToNew(_mappingContext)).ToArray()
);
var _result = new ItemResult();
_result.Object = Mapper.MapToDto(_methodResult, _mappingContext, includeTree);
diff --git a/playground/Coalesce.Web.Vue2/src/api-clients.g.ts b/playground/Coalesce.Web.Vue2/src/api-clients.g.ts
index b48d2a865..ad19ae1c9 100644
--- a/playground/Coalesce.Web.Vue2/src/api-clients.g.ts
+++ b/playground/Coalesce.Web.Vue2/src/api-clients.g.ts
@@ -269,10 +269,11 @@ export class PersonApiClient extends ModelApiClient<$models.Person> {
return this.$invoke($method, $params, $config)
}
- public methodWithEntityParameter(person: $models.Person | null, $config?: AxiosRequestConfig): AxiosPromise> {
+ public methodWithEntityParameter(person: $models.Person | null, people: $models.Person[] | null, $config?: AxiosRequestConfig): AxiosPromise> {
const $method = this.$metadata.methods.methodWithEntityParameter
const $params = {
person,
+ people,
}
return this.$invoke($method, $params, $config)
}
diff --git a/playground/Coalesce.Web.Vue2/src/metadata.g.ts b/playground/Coalesce.Web.Vue2/src/metadata.g.ts
index 336a0b869..928dae24b 100644
--- a/playground/Coalesce.Web.Vue2/src/metadata.g.ts
+++ b/playground/Coalesce.Web.Vue2/src/metadata.g.ts
@@ -812,6 +812,13 @@ export const Case = domain.types.Case = {
},
},
},
+ missingManyToManyFarSide: {
+ type: "dataSource",
+ name: "MissingManyToManyFarSide" as const,
+ displayName: "Missing Many To Many Far Side",
+ props: {
+ },
+ },
},
}
export const CaseDto = domain.types.CaseDto = {
@@ -1795,6 +1802,22 @@ export const Person = domain.types.Person = {
required: val => val != null || "Person is required.",
}
},
+ people: {
+ name: "people",
+ displayName: "People",
+ type: "collection",
+ itemType: {
+ name: "$collectionItem",
+ displayName: "",
+ role: "value",
+ type: "model",
+ get typeDef() { return (domain.types.Person as ModelType & { name: "Person" }) },
+ },
+ role: "value",
+ rules: {
+ required: val => val != null || "People is required.",
+ }
+ },
},
return: {
name: "$return",
diff --git a/playground/Coalesce.Web.Vue2/src/models.g.ts b/playground/Coalesce.Web.Vue2/src/models.g.ts
index 0117febb4..e344abb1a 100644
--- a/playground/Coalesce.Web.Vue2/src/models.g.ts
+++ b/playground/Coalesce.Web.Vue2/src/models.g.ts
@@ -174,6 +174,10 @@ export namespace Case {
return reactiveDataSource(this);
}
}
+
+ export class MissingManyToManyFarSide implements DataSource {
+ readonly $metadata = metadata.Case.dataSources.missingManyToManyFarSide
+ }
}
}
diff --git a/playground/Coalesce.Web.Vue2/src/viewmodels.g.ts b/playground/Coalesce.Web.Vue2/src/viewmodels.g.ts
index 2686d4cd0..9f8d465ad 100644
--- a/playground/Coalesce.Web.Vue2/src/viewmodels.g.ts
+++ b/playground/Coalesce.Web.Vue2/src/viewmodels.g.ts
@@ -635,9 +635,9 @@ export class PersonListViewModel extends ListViewModel<$models.Person, $apiClien
public get methodWithEntityParameter() {
const methodWithEntityParameter = this.$apiClient.$makeCaller(
this.$metadata.methods.methodWithEntityParameter,
- (c, person: $models.Person | null) => c.methodWithEntityParameter(person),
- () => ({person: null as $models.Person | null, }),
- (c, args) => c.methodWithEntityParameter(args.person))
+ (c, person: $models.Person | null, people: $models.Person[] | null) => c.methodWithEntityParameter(person, people),
+ () => ({person: null as $models.Person | null, people: null as $models.Person[] | null, }),
+ (c, args) => c.methodWithEntityParameter(args.person, args.people))
Object.defineProperty(this, 'methodWithEntityParameter', {value: methodWithEntityParameter});
return methodWithEntityParameter
diff --git a/playground/Coalesce.Web.Vue3/.vscode/settings.json b/playground/Coalesce.Web.Vue3/.vscode/settings.json
index ec3b37134..a091bf3db 100644
--- a/playground/Coalesce.Web.Vue3/.vscode/settings.json
+++ b/playground/Coalesce.Web.Vue3/.vscode/settings.json
@@ -1,4 +1,8 @@
{
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules\\typescript\\lib",
"dotnet.defaultSolution": "Coalesce.Web.Vue3.sln"
}
\ No newline at end of file
diff --git a/playground/Coalesce.Web.Vue3/Api/Generated/PersonController.g.cs b/playground/Coalesce.Web.Vue3/Api/Generated/PersonController.g.cs
index eaedbdf18..8ff962edc 100644
--- a/playground/Coalesce.Web.Vue3/Api/Generated/PersonController.g.cs
+++ b/playground/Coalesce.Web.Vue3/Api/Generated/PersonController.g.cs
@@ -839,11 +839,13 @@ [FromBody] MethodWithStringArrayParameterParameters _params
[Authorize]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
public virtual ItemResult MethodWithEntityParameter(
- [FromForm(Name = "person")] PersonParameter person)
+ [FromForm(Name = "person")] PersonParameter person,
+ [FromForm(Name = "people")] PersonParameter[] people)
{
var _params = new
{
- Person = !Request.Form.HasAnyValue(nameof(person)) ? null : person
+ Person = !Request.Form.HasAnyValue(nameof(person)) ? null : person,
+ People = !Request.Form.HasAnyValue(nameof(people)) ? null : people.ToList()
};
if (Context.Options.ValidateAttributesForMethods)
@@ -857,7 +859,8 @@ public virtual ItemResult MethodWithEntityParameter(
var _mappingContext = new MappingContext(Context);
var _methodResult = Coalesce.Domain.Person.MethodWithEntityParameter(
Db,
- _params.Person?.MapToNew(_mappingContext)
+ _params.Person?.MapToNew(_mappingContext),
+ _params.People?.Select(_m => _m.MapToNew(_mappingContext)).ToArray()
);
var _result = new ItemResult();
_result.Object = Mapper.MapToDto(_methodResult, _mappingContext, includeTree);
@@ -867,6 +870,7 @@ public virtual ItemResult MethodWithEntityParameter(
public class MethodWithEntityParameterParameters
{
public PersonParameter Person { get; set; }
+ public PersonParameter[] People { get; set; }
}
///
@@ -890,7 +894,8 @@ [FromBody] MethodWithEntityParameterParameters _params
var _mappingContext = new MappingContext(Context);
var _methodResult = Coalesce.Domain.Person.MethodWithEntityParameter(
Db,
- _params.Person?.MapToNew(_mappingContext)
+ _params.Person?.MapToNew(_mappingContext),
+ _params.People?.Select(_m => _m.MapToNew(_mappingContext)).ToArray()
);
var _result = new ItemResult();
_result.Object = Mapper.MapToDto(_methodResult, _mappingContext, includeTree);
diff --git a/playground/Coalesce.Web.Vue3/src/App.vue b/playground/Coalesce.Web.Vue3/src/App.vue
index 7393653e6..15a91bd21 100644
--- a/playground/Coalesce.Web.Vue3/src/App.vue
+++ b/playground/Coalesce.Web.Vue3/src/App.vue
@@ -12,13 +12,11 @@
label="Dark Mode"
v-model="darkMode"
hide-details
- class="ml-2"
+ class="mx-3"
density="compact"
/>
- Home
- Test
- Test2
+ Examples/TestsAuditSwaggerOpenAPI
@@ -46,10 +44,8 @@
-
-
-
-
+
+
diff --git a/playground/Coalesce.Web.Vue3/src/api-clients.g.ts b/playground/Coalesce.Web.Vue3/src/api-clients.g.ts
index b48d2a865..ad19ae1c9 100644
--- a/playground/Coalesce.Web.Vue3/src/api-clients.g.ts
+++ b/playground/Coalesce.Web.Vue3/src/api-clients.g.ts
@@ -269,10 +269,11 @@ export class PersonApiClient extends ModelApiClient<$models.Person> {
return this.$invoke($method, $params, $config)
}
- public methodWithEntityParameter(person: $models.Person | null, $config?: AxiosRequestConfig): AxiosPromise> {
+ public methodWithEntityParameter(person: $models.Person | null, people: $models.Person[] | null, $config?: AxiosRequestConfig): AxiosPromise> {
const $method = this.$metadata.methods.methodWithEntityParameter
const $params = {
person,
+ people,
}
return this.$invoke($method, $params, $config)
}
diff --git a/playground/Coalesce.Web.Vue3/src/components/Examples.vue b/playground/Coalesce.Web.Vue3/src/components/Examples.vue
new file mode 100644
index 000000000..5ff343140
--- /dev/null
+++ b/playground/Coalesce.Web.Vue3/src/components/Examples.vue
@@ -0,0 +1,67 @@
+
+
+