1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web): show partners assets on the main timeline (#4933)

This commit is contained in:
Alex 2023-11-11 15:06:19 -06:00 committed by GitHub
parent 3b11854702
commit 35767591d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1161 additions and 123 deletions

View File

@ -2361,6 +2361,103 @@ export interface OAuthConfigResponseDto {
*/
'url'?: string;
}
/**
*
* @export
* @interface PartnerResponseDto
*/
export interface PartnerResponseDto {
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'createdAt': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'deletedAt': string | null;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'email': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'externalPath': string | null;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'firstName': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'id': string;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'inTimeline'?: boolean;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'isAdmin': boolean;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'lastName': string;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'memoriesEnabled'?: boolean;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'oauthId': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'profileImagePath': string;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'shouldChangePassword': boolean;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'storageLabel': string | null;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'updatedAt': string;
}
/**
*
* @export
@ -4220,6 +4317,19 @@ export interface UpdateLibraryDto {
*/
'name'?: string;
}
/**
*
* @export
* @interface UpdatePartnerDto
*/
export interface UpdatePartnerDto {
/**
*
* @type {boolean}
* @memberof UpdatePartnerDto
*/
'inTimeline': boolean;
}
/**
*
* @export
@ -7274,11 +7384,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBucket', 'size', size)
// verify required parameter 'timeBucket' is not null or undefined
@ -7336,6 +7447,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked;
}
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (timeBucket !== undefined) {
localVarQueryParameter['timeBucket'] = timeBucket;
}
@ -7365,11 +7480,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBuckets', 'size', size)
const localVarPath = `/asset/time-buckets`;
@ -7425,6 +7541,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked;
}
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (key !== undefined) {
localVarQueryParameter['key'] = key;
}
@ -8227,12 +8347,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -8245,12 +8366,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -8547,7 +8669,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
*
@ -8556,7 +8678,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
* Get all asset of a device that are in the database, ID only.
@ -9043,6 +9165,13 @@ export interface AssetApiGetTimeBucketRequest {
*/
readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBucket
*/
readonly withPartners?: boolean
/**
*
* @type {string}
@ -9113,6 +9242,13 @@ export interface AssetApiGetTimeBucketsRequest {
*/
readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBuckets
*/
readonly withPartners?: boolean
/**
*
* @type {string}
@ -9592,7 +9728,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -9603,7 +9739,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -12312,6 +12448,54 @@ export const PartnerApiAxiosParamCreator = function (configuration?: Configurati
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {UpdatePartnerDto} updatePartnerDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updatePartner: async (id: string, updatePartnerDto: UpdatePartnerDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updatePartner', 'id', id)
// verify required parameter 'updatePartnerDto' is not null or undefined
assertParamExists('updatePartner', 'updatePartnerDto', updatePartnerDto)
const localVarPath = `/partner/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -12333,7 +12517,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PartnerResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -12343,7 +12527,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserResponseDto>>> {
async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PartnerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -12357,6 +12541,17 @@ export const PartnerApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {UpdatePartnerDto} updatePartnerDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updatePartner(id: string, updatePartnerDto: UpdatePartnerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PartnerResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartner(id, updatePartnerDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -12373,7 +12568,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<PartnerResponseDto> {
return localVarFp.createPartner(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
@ -12382,7 +12577,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise<Array<UserResponseDto>> {
getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PartnerResponseDto>> {
return localVarFp.getPartners(requestParameters.direction, options).then((request) => request(axios, basePath));
},
/**
@ -12394,6 +12589,15 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa
removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<PartnerResponseDto> {
return localVarFp.updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(axios, basePath));
},
};
};
@ -12439,6 +12643,27 @@ export interface PartnerApiRemovePartnerRequest {
readonly id: string
}
/**
* Request parameters for updatePartner operation in PartnerApi.
* @export
* @interface PartnerApiUpdatePartnerRequest
*/
export interface PartnerApiUpdatePartnerRequest {
/**
*
* @type {string}
* @memberof PartnerApiUpdatePartner
*/
readonly id: string
/**
*
* @type {UpdatePartnerDto}
* @memberof PartnerApiUpdatePartner
*/
readonly updatePartnerDto: UpdatePartnerDto
}
/**
* PartnerApi - object-oriented interface
* @export
@ -12478,6 +12703,17 @@ export class PartnerApi extends BaseAPI {
public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) {
return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PartnerApi
*/
public updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig) {
return PartnerApiFp(this.configuration).updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -187,12 +187,15 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
if (userResponseDto != null) {
Store.put(StoreKey.deviceId, deviceId);
Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
Store.put(
StoreKey.currentUser,
User.fromUserDto(userResponseDto),
);
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);
shouldChangePassword = userResponseDto.shouldChangePassword;
user = User.fromDto(userResponseDto);
user = User.fromUserDto(userResponseDto);
retResult = true;
} else {

View File

@ -36,7 +36,7 @@ class PartnerService {
final userDtos =
await _apiService.partnerApi.getPartners(direction._value);
if (userDtos != null) {
return userDtos.map((u) => User.fromDto(u)).toList();
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
}
} catch (e) {
_log.warning("failed to get partners for direction $direction:\n$e");

View File

@ -63,7 +63,10 @@ class TabNavigationObserver extends AutoRouterObserver {
return;
}
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
Store.put(
StoreKey.currentUser,
User.fromUserDto(userResponseDto),
);
ref.read(serverInfoProvider.notifier).getServerVersion();
} catch (e) {
debugPrint("Error refreshing user info $e");

View File

@ -18,11 +18,12 @@ class User {
this.isPartnerSharedWith = false,
this.profileImagePath = '',
this.memoryEnabled = true,
this.inTimeline = false,
});
Id get isarId => fastHash(id);
User.fromDto(UserResponseDto dto)
User.fromUserDto(UserResponseDto dto)
: id = dto.id,
updatedAt = dto.updatedAt,
email = dto.email,
@ -34,6 +35,19 @@ class User {
isAdmin = dto.isAdmin,
memoryEnabled = dto.memoriesEnabled;
User.fromPartnerDto(PartnerResponseDto dto)
: id = dto.id,
updatedAt = dto.updatedAt,
email = dto.email,
firstName = dto.firstName,
lastName = dto.lastName,
isPartnerSharedBy = false,
isPartnerSharedWith = false,
profileImagePath = dto.profileImagePath,
isAdmin = dto.isAdmin,
memoryEnabled = dto.memoriesEnabled,
inTimeline = dto.inTimeline;
@Index(unique: true, replace: false, type: IndexType.hash)
String id;
DateTime updatedAt;
@ -45,6 +59,8 @@ class User {
bool isAdmin;
String profileImagePath;
bool? memoryEnabled;
bool? inTimeline;
@Backlink(to: 'owner')
final IsarLinks<Album> albums = IsarLinks<Album>();
@Backlink(to: 'sharedUsers')
@ -62,7 +78,8 @@ class User {
isPartnerSharedWith == other.isPartnerSharedWith &&
profileImagePath == other.profileImagePath &&
isAdmin == other.isAdmin &&
memoryEnabled == other.memoryEnabled;
memoryEnabled == other.memoryEnabled &&
inTimeline == other.inTimeline;
}
@override
@ -77,5 +94,6 @@ class User {
isPartnerSharedWith.hashCode ^
profileImagePath.hashCode ^
isAdmin.hashCode ^
memoryEnabled.hashCode;
memoryEnabled.hashCode ^
inTimeline.hashCode;
}

Binary file not shown.

View File

@ -40,7 +40,7 @@ class UserService {
Future<List<User>?> _getAllUsers({required bool isAll}) async {
try {
final dto = await _apiService.userApi.getAllUsers(isAll);
return dto?.map(User.fromDto).toList();
return dto?.map(User.fromUserDto).toList();
} catch (e) {
_log.warning("Failed get all users:\n$e");
return null;

View File

@ -91,6 +91,7 @@ doc/OAuthCallbackDto.md
doc/OAuthConfigDto.md
doc/OAuthConfigResponseDto.md
doc/PartnerApi.md
doc/PartnerResponseDto.md
doc/PathEntityType.md
doc/PathType.md
doc/PeopleResponseDto.md
@ -159,6 +160,7 @@ doc/TranscodePolicy.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateLibraryDto.md
doc/UpdatePartnerDto.md
doc/UpdateStackParentDto.md
doc/UpdateTagDto.md
doc/UpdateUserDto.md
@ -273,6 +275,7 @@ lib/model/o_auth_authorize_response_dto.dart
lib/model/o_auth_callback_dto.dart
lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_dto.dart
lib/model/partner_response_dto.dart
lib/model/path_entity_type.dart
lib/model/path_type.dart
lib/model/people_response_dto.dart
@ -335,6 +338,7 @@ lib/model/transcode_policy.dart
lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart
lib/model/update_library_dto.dart
lib/model/update_partner_dto.dart
lib/model/update_stack_parent_dto.dart
lib/model/update_tag_dto.dart
lib/model/update_user_dto.dart
@ -432,6 +436,7 @@ test/o_auth_callback_dto_test.dart
test/o_auth_config_dto_test.dart
test/o_auth_config_response_dto_test.dart
test/partner_api_test.dart
test/partner_response_dto_test.dart
test/path_entity_type_test.dart
test/path_type_test.dart
test/people_response_dto_test.dart
@ -500,6 +505,7 @@ test/transcode_policy_test.dart
test/update_album_dto_test.dart
test/update_asset_dto_test.dart
test/update_library_dto_test.dart
test/update_partner_dto_test.dart
test/update_stack_parent_dto_test.dart
test/update_tag_dto_test.dart
test/update_user_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/PartnerResponseDto.md generated Normal file

Binary file not shown.

BIN
mobile/openapi/doc/UpdatePartnerDto.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2071,6 +2071,14 @@
"type": "boolean"
}
},
{
"name": "withPartners",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "timeBucket",
"required": true,
@ -2199,6 +2207,14 @@
"type": "boolean"
}
},
{
"name": "withPartners",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "key",
"required": false,
@ -3491,7 +3507,7 @@
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/UserResponseDto"
"$ref": "#/components/schemas/PartnerResponseDto"
},
"type": "array"
}
@ -3568,7 +3584,57 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserResponseDto"
"$ref": "#/components/schemas/PartnerResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Partner"
]
},
"put": {
"operationId": "updatePartner",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdatePartnerDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PartnerResponseDto"
}
}
},
@ -7572,6 +7638,77 @@
],
"type": "object"
},
"PartnerResponseDto": {
"properties": {
"createdAt": {
"format": "date-time",
"type": "string"
},
"deletedAt": {
"format": "date-time",
"nullable": true,
"type": "string"
},
"email": {
"type": "string"
},
"externalPath": {
"nullable": true,
"type": "string"
},
"firstName": {
"type": "string"
},
"id": {
"type": "string"
},
"inTimeline": {
"type": "boolean"
},
"isAdmin": {
"type": "boolean"
},
"lastName": {
"type": "string"
},
"memoriesEnabled": {
"type": "boolean"
},
"oauthId": {
"type": "string"
},
"profileImagePath": {
"type": "string"
},
"shouldChangePassword": {
"type": "boolean"
},
"storageLabel": {
"nullable": true,
"type": "string"
},
"updatedAt": {
"format": "date-time",
"type": "string"
}
},
"required": [
"id",
"firstName",
"lastName",
"email",
"profileImagePath",
"storageLabel",
"externalPath",
"shouldChangePassword",
"isAdmin",
"createdAt",
"deletedAt",
"updatedAt",
"oauthId"
],
"type": "object"
},
"PathEntityType": {
"enum": [
"asset",
@ -8982,6 +9119,17 @@
},
"type": "object"
},
"UpdatePartnerDto": {
"properties": {
"inTimeline": {
"type": "boolean"
}
},
"required": [
"inTimeline"
],
"type": "object"
},
"UpdateStackParentDto": {
"properties": {
"newParentId": {

View File

@ -40,6 +40,8 @@ export enum Permission {
PERSON_READ = 'person.read',
PERSON_WRITE = 'person.write',
PERSON_MERGE = 'person.merge',
PARTNER_UPDATE = 'partner.update',
}
let instance: AccessCore | null;
@ -242,6 +244,9 @@ export class AccessCore {
case Permission.PERSON_MERGE:
return this.repository.person.hasOwnerAccess(authUser.id, id);
case Permission.PARTNER_UPDATE:
return this.repository.partner.hasUpdateAccess(authUser.id, id);
default:
return false;
}

View File

@ -10,6 +10,7 @@ import {
newCommunicationRepositoryMock,
newCryptoRepositoryMock,
newJobRepositoryMock,
newPartnerRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
} from '@test';
@ -23,6 +24,7 @@ import {
ICommunicationRepository,
ICryptoRepository,
IJobRepository,
IPartnerRepository,
IStorageRepository,
ISystemConfigRepository,
JobItem,
@ -164,6 +166,7 @@ describe(AssetService.name, () => {
let storageMock: jest.Mocked<IStorageRepository>;
let communicationMock: jest.Mocked<ICommunicationRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let partnerMock: jest.Mocked<IPartnerRepository>;
it('should work', () => {
expect(sut).toBeDefined();
@ -177,7 +180,18 @@ describe(AssetService.name, () => {
jobMock = newJobRepositoryMock();
storageMock = newStorageRepositoryMock();
configMock = newSystemConfigRepositoryMock();
sut = new AssetService(accessMock, assetMock, cryptoMock, jobMock, configMock, storageMock, communicationMock);
partnerMock = newPartnerRepositoryMock();
sut = new AssetService(
accessMock,
assetMock,
cryptoMock,
jobMock,
configMock,
storageMock,
communicationMock,
partnerMock,
);
when(assetMock.getById)
.calledWith(assetStub.livePhotoStillAsset.id)
@ -327,7 +341,7 @@ describe(AssetService.name, () => {
size: TimeBucketSize.DAY,
}),
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userId: authStub.admin.id });
expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.id] });
});
});
@ -363,7 +377,7 @@ describe(AssetService.name, () => {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
userId: authStub.admin.id,
userIds: [authStub.admin.id],
});
});
@ -380,9 +394,65 @@ describe(AssetService.name, () => {
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userId: authStub.admin.id,
userIds: [authStub.admin.id],
});
});
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
withPartners: true,
userId: authStub.admin.id,
}),
).rejects.toThrowError(BadRequestException);
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: undefined,
withPartners: true,
userId: authStub.admin.id,
}),
).rejects.toThrowError(BadRequestException);
});
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isFavorite: true,
withPartners: true,
userId: authStub.admin.id,
}),
).rejects.toThrowError(BadRequestException);
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isFavorite: false,
withPartners: true,
userId: authStub.admin.id,
}),
).rejects.toThrowError(BadRequestException);
});
it('should throw an error if withParners is true and isTrash is true', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isTrashed: true,
withPartners: true,
userId: authStub.admin.id,
}),
).rejects.toThrowError(BadRequestException);
});
});
describe('downloadFile', () => {

View File

@ -16,9 +16,11 @@ import {
ICommunicationRepository,
ICryptoRepository,
IJobRepository,
IPartnerRepository,
IStorageRepository,
ISystemConfigRepository,
ImmichReadStream,
TimeBucketOptions,
} from '../repositories';
import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config';
@ -83,6 +85,7 @@ export class AssetService {
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) {
this.access = AccessCore.create(accessRepository);
this.configCore = SystemConfigCore.create(configRepository);
@ -187,11 +190,25 @@ export class AssetService {
await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]);
}
}
if (dto.withPartners) {
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const requestedTrash = dto.isTrashed === true;
if (requestedArchived || requestedFavorite || requestedTrash) {
throw new BadRequestException(
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
);
}
}
}
async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(authUser, dto);
return this.assetRepository.getTimeBuckets(dto);
const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto);
return this.assetRepository.getTimeBuckets(timeBucketOptions);
}
async getTimeBucket(
@ -199,7 +216,8 @@ export class AssetService {
dto: TimeBucketAssetDto,
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
await this.timeBucketChecks(authUser, dto);
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto);
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
if (authUser.isShowMetadata) {
return assets.map((asset) => mapAsset(asset, { withStack: true }));
} else {
@ -207,6 +225,25 @@ export class AssetService {
}
}
async buildTimeBucketOptions(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
const { userId, ...options } = dto;
let userIds: string[] | undefined = undefined;
if (userId) {
userIds = [userId];
if (dto.withPartners) {
const partners = await this.partnerRepository.getAll(authUser.id);
const partnersIds = partners
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
.map((partner) => partner.sharedById);
userIds.push(...partnersIds);
}
}
return { ...options, userIds };
}
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);

View File

@ -38,6 +38,11 @@ export class TimeBucketDto {
@IsBoolean()
@Transform(toBoolean)
withStacked?: boolean;
@Optional()
@IsBoolean()
@Transform(toBoolean)
withPartners?: boolean;
}
export class TimeBucketAssetDto extends TimeBucketDto {

View File

@ -0,0 +1,11 @@
import { IsNotEmpty } from 'class-validator';
import { UserResponseDto } from '../user';
export class UpdatePartnerDto {
@IsNotEmpty()
inTimeline!: boolean;
}
export class PartnerResponseDto extends UserResponseDto {
inTimeline?: boolean;
}

View File

@ -1,11 +1,11 @@
import { BadRequestException } from '@nestjs/common';
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
import { UserResponseDto } from '../index';
import { IPartnerRepository, PartnerDirection } from '../repositories';
import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories';
import { PartnerResponseDto } from './partner.dto';
import { PartnerService } from './partner.service';
const responseDto = {
admin: <UserResponseDto>{
admin: <PartnerResponseDto>{
email: 'admin@test.com',
firstName: 'admin_first_name',
id: 'admin_id',
@ -20,8 +20,9 @@ const responseDto = {
updatedAt: new Date('2021-01-01'),
externalPath: null,
memoriesEnabled: true,
inTimeline: true,
},
user1: <UserResponseDto>{
user1: <PartnerResponseDto>{
email: 'immich@test.com',
firstName: 'immich_first_name',
id: 'user-id',
@ -36,16 +37,18 @@ const responseDto = {
updatedAt: new Date('2021-01-01'),
externalPath: null,
memoriesEnabled: true,
inTimeline: true,
},
};
describe(PartnerService.name, () => {
let sut: PartnerService;
let partnerMock: jest.Mocked<IPartnerRepository>;
let accessMock: jest.Mocked<IAccessRepository>;
beforeEach(async () => {
partnerMock = newPartnerRepositoryMock();
sut = new PartnerService(partnerMock);
sut = new PartnerService(partnerMock, accessMock);
});
it('should work', () => {

View File

@ -1,14 +1,22 @@
import { PartnerEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth';
import { IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories';
import { UserResponseDto, mapUser } from '../user';
import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories';
import { mapUser } from '../user';
import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto';
@Injectable()
export class PartnerService {
constructor(@Inject(IPartnerRepository) private repository: IPartnerRepository) {}
private access: AccessCore;
constructor(
@Inject(IPartnerRepository) private repository: IPartnerRepository,
@Inject(IAccessRepository) accessRepository: IAccessRepository,
) {
this.access = AccessCore.create(accessRepository);
}
async create(authUser: AuthUserDto, sharedWithId: string): Promise<UserResponseDto> {
async create(authUser: AuthUserDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId };
const exists = await this.repository.get(partnerId);
if (exists) {
@ -29,7 +37,7 @@ export class PartnerService {
await this.repository.remove(partner);
}
async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise<UserResponseDto[]> {
async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
const partners = await this.repository.getAll(authUser.id);
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners
@ -38,8 +46,22 @@ export class PartnerService {
.map((partner) => this.map(partner, direction));
}
private map(partner: PartnerEntity, direction: PartnerDirection): UserResponseDto {
async update(authUser: AuthUserDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
await this.access.requirePermission(authUser, Permission.PARTNER_UPDATE, sharedById);
const partnerId: PartnerIds = { sharedById, sharedWithId: authUser.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
return this.map(entity, PartnerDirection.SharedWith);
}
private map(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
// this is opposite to return the non-me user of the "partner"
return mapUser(direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy);
const user = mapUser(
direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
) as PartnerResponseDto;
user.inTimeline = partner.inTimeline;
return user;
}
}

View File

@ -35,4 +35,8 @@ export interface IAccessRepository {
person: {
hasOwnerAccess(userId: string, personId: string): Promise<boolean>;
};
partner: {
hasUpdateAccess(userId: string, partnerId: string): Promise<boolean>;
};
}

View File

@ -65,7 +65,7 @@ export interface TimeBucketOptions {
isTrashed?: boolean;
albumId?: string;
personId?: string;
userId?: string;
userIds?: string[];
withStacked?: boolean;
}

View File

@ -17,4 +17,5 @@ export interface IPartnerRepository {
get(partner: PartnerIds): Promise<PartnerEntity | null>;
create(partner: PartnerIds): Promise<PartnerEntity>;
remove(entity: PartnerEntity): Promise<void>;
update(entity: Partial<PartnerEntity>): Promise<PartnerEntity>;
}

View File

@ -1,5 +1,6 @@
import { AuthUserDto, PartnerDirection, PartnerService, UserResponseDto } from '@app/domain';
import { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
import { AuthUserDto, PartnerDirection, PartnerService } from '@app/domain';
import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto';
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
@ -17,15 +18,24 @@ export class PartnerController {
getPartners(
@AuthUser() authUser: AuthUserDto,
@Query('direction') direction: PartnerDirection,
): Promise<UserResponseDto[]> {
): Promise<PartnerResponseDto[]> {
return this.service.getAll(authUser, direction);
}
@Post(':id')
createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(authUser, id);
}
@Put(':id')
updatePartner(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdatePartnerDto,
): Promise<PartnerResponseDto> {
return this.service.update(authUser, id, dto);
}
@Delete(':id')
removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id);

View File

@ -1,4 +1,4 @@
import { CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
import { UserEntity } from './user.entity';
@ -23,4 +23,7 @@ export class PartnerEntity {
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date;
@Column({ type: 'boolean', default: false })
inTimeline!: boolean;
}

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AdddInTimelineToPartnersTable1699562570201 implements MigrationInterface {
name = 'AdddInTimelineToPartnersTable1699562570201'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "partners" ADD "inTimeline" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "partners" DROP COLUMN "inTimeline"`);
}
}

View File

@ -249,4 +249,15 @@ export class AccessRepository implements IAccessRepository {
});
},
};
partner = {
hasUpdateAccess: (userId: string, partnerId: string): Promise<boolean> => {
return this.partnerRepository.exist({
where: {
sharedById: partnerId,
sharedWithId: userId,
},
});
},
};
}

View File

@ -519,7 +519,7 @@ export class AssetRepository implements IAssetRepository {
}
private getBuilder(options: TimeBucketOptions) {
const { isArchived, isFavorite, isTrashed, albumId, personId, userId, withStacked } = options;
const { isArchived, isFavorite, isTrashed, albumId, personId, userIds, withStacked } = options;
let builder = this.repository
.createQueryBuilder('asset')
@ -532,11 +532,11 @@ export class AssetRepository implements IAssetRepository {
builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId });
}
if (userId) {
builder = builder.andWhere('asset.ownerId = :userId', { userId });
if (userIds) {
builder = builder.andWhere('asset.ownerId IN (:...userIds )', { userIds });
}
if (isArchived != undefined) {
if (isArchived !== undefined) {
builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived });
}

View File

@ -1,7 +1,7 @@
import { IPartnerRepository, PartnerIds } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DeepPartial, Repository } from 'typeorm';
import { PartnerEntity } from '../entities';
@Injectable()
@ -16,12 +16,22 @@ export class PartnerRepository implements IPartnerRepository {
return this.repository.findOne({ where: { sharedById, sharedWithId } });
}
async create({ sharedById, sharedWithId }: PartnerIds): Promise<PartnerEntity> {
await this.repository.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } });
return this.repository.findOneOrFail({ where: { sharedById, sharedWithId } });
create({ sharedById, sharedWithId }: PartnerIds): Promise<PartnerEntity> {
return this.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } });
}
async remove(entity: PartnerEntity): Promise<void> {
await this.repository.remove(entity);
}
update(entity: Partial<PartnerEntity>): Promise<PartnerEntity> {
return this.save(entity);
}
private async save(entity: DeepPartial<PartnerEntity>): Promise<PartnerEntity> {
await this.repository.save(entity);
return this.repository.findOneOrFail({
where: { sharedById: entity.sharedById, sharedWithId: entity.sharedWithId },
});
}
}

View File

@ -584,6 +584,52 @@ describe(`${AssetController.name} (e2e)`, () => {
]),
);
});
it('should return error if time bucket is requested with partners asset and archived', async () => {
const req1 = await request(server)
.get('/asset/time-buckets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorStub.badRequest());
const req2 = await request(server)
.get('/asset/time-buckets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: undefined });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorStub.badRequest());
});
it('should return error if time bucket is requested with partners asset and favorite', async () => {
const req1 = await request(server)
.get('/asset/time-buckets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: true });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorStub.badRequest());
const req2 = await request(server)
.get('/asset/time-buckets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: false });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorStub.badRequest());
});
it('should return error if time bucket is requested with partners asset and trash', async () => {
const req = await request(server)
.get('/asset/time-buckets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.query({ size: TimeBucketSize.MONTH, withPartners: true, isTrashed: true });
expect(req.status).toBe(400);
expect(req.body).toEqual(errorStub.badRequest());
});
});
describe('GET /asset/map-marker', () => {

View File

@ -115,6 +115,26 @@ describe(`${PartnerController.name} (e2e)`, () => {
});
});
describe('PUT /partner/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).put(`/partner/${user2.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should update partner', async () => {
await repository.create({ sharedById: user2.userId, sharedWithId: user1.userId });
const { status, body } = await request(server)
.put(`/partner/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ inTimeline: false });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
});
});
describe('DELETE /partner/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).delete(`/partner/${user2.userId}`);

View File

@ -9,6 +9,7 @@ export const partnerStub = {
sharedBy: userStub.admin,
sharedWith: userStub.user1,
sharedWithId: userStub.user1.id,
inTimeline: true,
}),
user1ToAdmin1: Object.freeze<PartnerEntity>({
createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -17,5 +18,6 @@ export const partnerStub = {
sharedById: userStub.user1.id,
sharedWithId: userStub.admin.id,
sharedWith: userStub.admin,
inTimeline: true,
}),
};

View File

@ -8,6 +8,7 @@ export interface IAccessRepositoryMock {
library: jest.Mocked<IAccessRepository['library']>;
timeline: jest.Mocked<IAccessRepository['timeline']>;
person: jest.Mocked<IAccessRepository['person']>;
partner: jest.Mocked<IAccessRepository['partner']>;
}
export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => {
@ -50,5 +51,9 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
person: {
hasOwnerAccess: jest.fn(),
},
partner: {
hasUpdateAccess: jest.fn(),
},
};
};

View File

@ -6,5 +6,6 @@ export const newPartnerRepositoryMock = (): jest.Mocked<IPartnerRepository> => {
remove: jest.fn(),
getAll: jest.fn(),
get: jest.fn(),
update: jest.fn(),
};
};

View File

@ -2361,6 +2361,103 @@ export interface OAuthConfigResponseDto {
*/
'url'?: string;
}
/**
*
* @export
* @interface PartnerResponseDto
*/
export interface PartnerResponseDto {
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'createdAt': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'deletedAt': string | null;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'email': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'externalPath': string | null;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'firstName': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'id': string;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'inTimeline'?: boolean;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'isAdmin': boolean;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'lastName': string;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'memoriesEnabled'?: boolean;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'oauthId': string;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'profileImagePath': string;
/**
*
* @type {boolean}
* @memberof PartnerResponseDto
*/
'shouldChangePassword': boolean;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'storageLabel': string | null;
/**
*
* @type {string}
* @memberof PartnerResponseDto
*/
'updatedAt': string;
}
/**
*
* @export
@ -4220,6 +4317,19 @@ export interface UpdateLibraryDto {
*/
'name'?: string;
}
/**
*
* @export
* @interface UpdatePartnerDto
*/
export interface UpdatePartnerDto {
/**
*
* @type {boolean}
* @memberof UpdatePartnerDto
*/
'inTimeline': boolean;
}
/**
*
* @export
@ -7274,11 +7384,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBucket', 'size', size)
// verify required parameter 'timeBucket' is not null or undefined
@ -7336,6 +7447,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked;
}
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (timeBucket !== undefined) {
localVarQueryParameter['timeBucket'] = timeBucket;
}
@ -7365,11 +7480,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBuckets', 'size', size)
const localVarPath = `/asset/time-buckets`;
@ -7425,6 +7541,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked;
}
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (key !== undefined) {
localVarQueryParameter['key'] = key;
}
@ -8227,12 +8347,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -8245,12 +8366,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite]
* @param {boolean} [isTrashed]
* @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -8547,7 +8669,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
*
@ -8556,7 +8678,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError}
*/
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
* Get all asset of a device that are in the database, ID only.
@ -9043,6 +9165,13 @@ export interface AssetApiGetTimeBucketRequest {
*/
readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBucket
*/
readonly withPartners?: boolean
/**
*
* @type {string}
@ -9113,6 +9242,13 @@ export interface AssetApiGetTimeBucketsRequest {
*/
readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBuckets
*/
readonly withPartners?: boolean
/**
*
* @type {string}
@ -9592,7 +9728,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -9603,7 +9739,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
@ -12312,6 +12448,54 @@ export const PartnerApiAxiosParamCreator = function (configuration?: Configurati
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {UpdatePartnerDto} updatePartnerDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updatePartner: async (id: string, updatePartnerDto: UpdatePartnerDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updatePartner', 'id', id)
// verify required parameter 'updatePartnerDto' is not null or undefined
assertParamExists('updatePartner', 'updatePartnerDto', updatePartnerDto)
const localVarPath = `/partner/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication cookie required
// authentication api_key required
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -12333,7 +12517,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PartnerResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -12343,7 +12527,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserResponseDto>>> {
async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PartnerResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -12357,6 +12541,17 @@ export const PartnerApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {UpdatePartnerDto} updatePartnerDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updatePartner(id: string, updatePartnerDto: UpdatePartnerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PartnerResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartner(id, updatePartnerDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
@ -12373,7 +12568,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<PartnerResponseDto> {
return localVarFp.createPartner(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
@ -12382,7 +12577,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise<Array<UserResponseDto>> {
getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PartnerResponseDto>> {
return localVarFp.getPartners(requestParameters.direction, options).then((request) => request(axios, basePath));
},
/**
@ -12394,6 +12589,15 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa
removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<PartnerResponseDto> {
return localVarFp.updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(axios, basePath));
},
};
};
@ -12439,6 +12643,27 @@ export interface PartnerApiRemovePartnerRequest {
readonly id: string
}
/**
* Request parameters for updatePartner operation in PartnerApi.
* @export
* @interface PartnerApiUpdatePartnerRequest
*/
export interface PartnerApiUpdatePartnerRequest {
/**
*
* @type {string}
* @memberof PartnerApiUpdatePartner
*/
readonly id: string
/**
*
* @type {UpdatePartnerDto}
* @memberof PartnerApiUpdatePartner
*/
readonly updatePartnerDto: UpdatePartnerDto
}
/**
* PartnerApi - object-oriented interface
* @export
@ -12478,6 +12703,17 @@ export class PartnerApi extends BaseAPI {
public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) {
return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PartnerApi
*/
public updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig) {
return PartnerApiFp(this.configuration).updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@ -97,8 +97,12 @@
</script>
<header>
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
{#if $isMultiSelectState && user}
<AssetSelectControlBar
ownerId={user.id}
assets={$selectedAssets}
clearSelect={() => assetInteractionStore.clearMultiselect()}
>
<SelectAllAssets {assetStore} {assetInteractionStore} />
{#if sharedLink.allowDownload}
<DownloadAction filename="{album.albumName}.zip" />

View File

@ -20,14 +20,14 @@
let loading = false;
const { getAssets, clearSelect } = getAssetControlContext();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleArchive = async () => {
const isArchived = !unarchive;
loading = true;
try {
const assets = Array.from(getAssets()).filter((asset) => asset.isArchived !== isArchived);
const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isArchived !== isArchived);
const ids = assets.map(({ id }) => id);
if (ids.length > 0) {

View File

@ -14,13 +14,13 @@
AssetJobName.TranscodeVideo,
];
const { getAssets, clearSelect } = getAssetControlContext();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
$: isAllVideos = Array.from(getAssets()).every((asset) => asset.type === AssetTypeEnum.Video);
$: isAllVideos = Array.from(getOwnedAssets()).every((asset) => asset.type === AssetTypeEnum.Video);
const handleRunJob = async (name: AssetJobName) => {
try {
const ids = Array.from(getAssets()).map(({ id }) => id);
const ids = Array.from(getOwnedAssets()).map(({ id }) => id);
await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info });
clearSelect();

View File

@ -17,7 +17,7 @@
export let menuItem = false;
export let force = !$featureFlags.trash;
const { getAssets, clearSelect } = getAssetControlContext();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const dispatch = createEventDispatcher();
@ -37,7 +37,7 @@
loading = true;
try {
const ids = Array.from(getAssets())
const ids = Array.from(getOwnedAssets())
.filter((a) => !a.isExternal)
.map((a) => a.id);
await api.assetApi.deleteAssets({ assetBulkDeleteDto: { ids, force } });
@ -75,7 +75,7 @@
{#if isShowConfirmation}
<ConfirmDialogue
title="Permanently Delete Asset{getAssets().size > 1 ? 's' : ''}"
title="Permanently Delete Asset{getOwnedAssets().size > 1 ? 's' : ''}"
confirmText="Delete"
on:confirm={handleDelete}
on:cancel={() => (isShowConfirmation = false)}
@ -84,8 +84,8 @@
<svelte:fragment slot="prompt">
<p>
Are you sure you want to permanently delete
{#if getAssets().size > 1}
these <b>{getAssets().size}</b> assets? This will also remove them from their album(s).
{#if getOwnedAssets().size > 1}
these <b>{getOwnedAssets().size}</b> assets? This will also remove them from their album(s).
{:else}
this asset? This will also remove it from its album(s).
{/if}

View File

@ -20,14 +20,15 @@
let loading = false;
const { getAssets, clearSelect } = getAssetControlContext();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleFavorite = async () => {
const isFavorite = !removeFavorite;
loading = true;
try {
const assets = Array.from(getAssets()).filter((asset) => asset.isFavorite !== isFavorite);
const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isFavorite !== isFavorite);
const ids = assets.map(({ id }) => id);
if (ids.length > 0) {

View File

@ -10,11 +10,11 @@
export let onStack: OnStack | undefined = undefined;
const { getAssets, clearSelect } = getAssetControlContext();
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleStack = async () => {
try {
const assets = Array.from(getAssets());
const assets = Array.from(getOwnedAssets());
const parent = assets.at(0);
if (parent == undefined) {

View File

@ -9,7 +9,8 @@
export interface AssetControlContext {
// Wrap assets in a function, because context isn't reactive.
getAssets: () => Set<AssetResponseDto>;
getAssets: () => Set<AssetResponseDto>; // All assets includes partners' assets
getOwnedAssets: () => Set<AssetResponseDto>; // Only assets owned by the user
clearSelect: () => void;
}
@ -25,8 +26,14 @@
export let assets: Set<AssetResponseDto>;
export let clearSelect: () => void;
export let ownerId: string | undefined = undefined;
setContext({ getAssets: () => assets, clearSelect });
setContext({
getAssets: () => assets,
getOwnedAssets: () =>
ownerId !== undefined ? new Set(Array.from(assets).filter((asset) => asset.ownerId === ownerId)) : assets,
clearSelect,
});
</script>
<ControlAppBar on:close-button-click={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">

View File

@ -1,22 +1,71 @@
<script lang="ts">
import { UserResponseDto, api } from '@api';
import { PartnerResponseDto, UserResponseDto, api } from '@api';
import UserAvatar from '../shared-components/user-avatar.svelte';
import Button from '../elements/buttons/button.svelte';
import PartnerSelectionModal from './partner-selection-modal.svelte';
import { handleError } from '../../utils/handle-error';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { mdiClose } from '@mdi/js';
import { mdiCheck, mdiClose } from '@mdi/js';
import { onMount } from 'svelte';
import Icon from '../elements/icon.svelte';
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
interface PartnerSharing {
user: UserResponseDto;
sharedByMe: boolean;
sharedWithMe: boolean;
inTimeline: boolean;
}
export let user: UserResponseDto;
export let partners: UserResponseDto[];
let createPartner = false;
let removePartner: UserResponseDto | null = null;
let removePartner: PartnerResponseDto | null = null;
let partners: Array<PartnerSharing> = [];
onMount(() => {
refreshPartners();
});
const refreshPartners = async () => {
const { data } = await api.partnerApi.getPartners({ direction: 'shared-by' });
partners = data;
partners = [];
const [{ data: sharedBy }, { data: sharedWith }] = await Promise.all([
api.partnerApi.getPartners({ direction: 'shared-by' }),
api.partnerApi.getPartners({ direction: 'shared-with' }),
]);
for (const candidate of sharedBy) {
partners = [
...partners,
{
user: candidate,
sharedByMe: true,
sharedWithMe: false,
inTimeline: candidate.inTimeline ?? false,
},
];
}
for (const candidate of sharedWith) {
const existIndex = partners.findIndex((p) => candidate.id === p.user.id);
if (existIndex >= 0) {
partners[existIndex].sharedWithMe = true;
partners[existIndex].inTimeline = candidate.inTimeline ?? false;
} else {
partners = [
...partners,
{
user: candidate,
sharedByMe: false,
sharedWithMe: true,
inTimeline: candidate.inTimeline ?? false,
},
];
}
}
};
const handleRemovePartner = async () => {
@ -45,34 +94,80 @@
handleError(error, 'Unable to add partners');
}
};
const handleShowOnTimelineChanged = async (partner: PartnerSharing, inTimeline: boolean) => {
try {
await api.partnerApi.updatePartner({ id: partner.user.id, updatePartnerDto: { inTimeline } });
partner.inTimeline = inTimeline;
partners = partners;
} catch (error) {
handleError(error, 'Unable to update timeline display status');
}
};
</script>
<section class="my-4">
{#if partners.length > 0}
<div class="flex flex-row gap-4">
{#each partners as partner (partner.id)}
<div class="flex gap-4 rounded-lg px-5 py-4 transition-all">
<UserAvatar user={partner} size="md" autoColor />
{#each partners as partner (partner.user.id)}
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 mt-6 bg-slate-50 dark:bg-gray-900 p-5">
<div class="flex gap-4 rounded-lg pb-4 transition-all justify-between">
<div class="flex gap-4">
<UserAvatar user={partner.user} size="md" autoColor />
<div class="text-left">
<p class="text-immich-fg dark:text-immich-dark-fg">
{partner.firstName}
{partner.lastName}
{partner.user.firstName}
{partner.user.lastName}
</p>
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
{partner.email}
{partner.user.email}
</p>
</div>
</div>
{#if partner.sharedByMe}
<CircleIconButton
on:click={() => (removePartner = partner)}
on:click={() => (removePartner = partner.user)}
icon={mdiClose}
size={'16'}
title="Remove partner"
title="Stop sharing your photos with this user"
/>
{/if}
</div>
<div class="dark:text-gray-200 text-immich-dark-gray">
<!-- I am sharing my assets with this user -->
{#if partner.sharedByMe}
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
<p class="text-xs font-medium my-4">SHARED WITH {partner.user.firstName.toUpperCase()}</p>
<p class="text-md">{partner.user.firstName} can access</p>
<ul class="text-sm">
<li class="flex gap-2 place-items-center py-1 mt-2">
<Icon path={mdiCheck} /> All your photos and videos except those in Archived and Deleted
</li>
<li class="flex gap-2 place-items-center py-1">
<Icon path={mdiCheck} /> The location where your photos were taken
</li>
</ul>
{/if}
<!-- this user is sharing assets with me -->
{#if partner.sharedWithMe}
<hr class="my-4 border border-gray-200 dark:border-gray-700" />
<p class="text-xs font-medium my-4">PHOTOS FROM {partner.user.firstName.toUpperCase()}</p>
<SettingSwitch
title="Show in timeline"
subtitle="Show photos and videos from this user in your timeline"
bind:checked={partner.inTimeline}
on:toggle={({ detail }) => handleShowOnTimelineChanged(partner, detail)}
/>
{/if}
</div>
</div>
{/each}
</div>
{/if}
<div class="flex justify-end">
<div class="flex justify-end mt-5">
<Button size="sm" on:click={() => (createPartner = true)}>Add partner</Button>
</div>
</section>

View File

@ -18,7 +18,6 @@
export let keys: APIKeyResponseDto[] = [];
export let devices: AuthDeviceResponseDto[] = [];
export let partners: UserResponseDto[] = [];
let oauthOpen = false;
if (browser) {
@ -61,7 +60,7 @@
</SettingAccordion>
<SettingAccordion title="Sharing" subtitle="Manage sharing with partners">
<PartnerSettings {user} bind:partners />
<PartnerSettings {user} />
</SettingAccordion>
<SettingAccordion title="Sidebar" subtitle="Manage sidebar settings">

View File

@ -16,7 +16,7 @@
export let data: PageData;
const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false });
const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true });
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;

View File

@ -26,7 +26,7 @@
let { isViewing: showAssetViewer } = assetViewingStore;
let handleEscapeKey = false;
const assetStore = new AssetStore({ isArchived: false, withStacked: true });
const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true });
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@ -48,7 +48,11 @@
</script>
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<AssetSelectControlBar
ownerId={data.user.id}
assets={$selectedAssets}
clearSelect={() => assetInteractionStore.clearMultiselect()}
>
<CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
<SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={mdiPlus} title="Add">

View File

@ -10,13 +10,11 @@ export const load = (async ({ parent, locals }) => {
const { data: keys } = await locals.api.keyApi.getApiKeys();
const { data: devices } = await locals.api.authenticationApi.getAuthDevices();
const { data: partners } = await locals.api.partnerApi.getPartners({ direction: 'shared-by' });
return {
user,
keys,
devices,
partners,
meta: {
title: 'Settings',
},

View File

@ -9,7 +9,7 @@
<UserPageLayout user={data.user} title={data.meta.title}>
<section class="mx-4 flex place-content-center">
<div class="w-full max-w-3xl">
<UserSettingsList user={data.user} keys={data.keys} devices={data.devices} partners={data.partners} />
<UserSettingsList user={data.user} keys={data.keys} devices={data.devices} />
</div>
</section>
</UserPageLayout>