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; '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 * @export
@ -4220,6 +4317,19 @@ export interface UpdateLibraryDto {
*/ */
'name'?: string; 'name'?: string;
} }
/**
*
* @export
* @interface UpdatePartnerDto
*/
export interface UpdatePartnerDto {
/**
*
* @type {boolean}
* @memberof UpdatePartnerDto
*/
'inTimeline': boolean;
}
/** /**
* *
* @export * @export
@ -7274,11 +7384,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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 // verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBucket', 'size', size) assertParamExists('getTimeBucket', 'size', size)
// verify required parameter 'timeBucket' is not null or undefined // verify required parameter 'timeBucket' is not null or undefined
@ -7336,6 +7447,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked; localVarQueryParameter['withStacked'] = withStacked;
} }
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (timeBucket !== undefined) { if (timeBucket !== undefined) {
localVarQueryParameter['timeBucket'] = timeBucket; localVarQueryParameter['timeBucket'] = timeBucket;
} }
@ -7365,11 +7480,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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 // verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBuckets', 'size', size) assertParamExists('getTimeBuckets', 'size', size)
const localVarPath = `/asset/time-buckets`; const localVarPath = `/asset/time-buckets`;
@ -7425,6 +7541,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked; localVarQueryParameter['withStacked'] = withStacked;
} }
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (key !== undefined) { if (key !== undefined) {
localVarQueryParameter['key'] = key; localVarQueryParameter['key'] = key;
} }
@ -8227,12 +8347,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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>>> { 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, key, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -8245,12 +8366,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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>>> { 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, key, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -8547,7 +8669,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { 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} * @throws {RequiredError}
*/ */
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { 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. * Get all asset of a device that are in the database, ID only.
@ -9043,6 +9165,13 @@ export interface AssetApiGetTimeBucketRequest {
*/ */
readonly withStacked?: boolean readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBucket
*/
readonly withPartners?: boolean
/** /**
* *
* @type {string} * @type {string}
@ -9113,6 +9242,13 @@ export interface AssetApiGetTimeBucketsRequest {
*/ */
readonly withStacked?: boolean readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBuckets
*/
readonly withPartners?: boolean
/** /**
* *
* @type {string} * @type {string}
@ -9592,7 +9728,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi * @memberof AssetApi
*/ */
public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) { 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 * @memberof AssetApi
*/ */
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { 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 : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.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 { return {
url: toPathString(localVarUrlObj), url: toPathString(localVarUrlObj),
options: localVarRequestOptions, options: localVarRequestOptions,
@ -12333,7 +12517,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@ -12343,7 +12527,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@ -12357,6 +12541,17 @@ export const PartnerApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options); const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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)); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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)); 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> { removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath)); 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 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 * PartnerApi - object-oriented interface
* @export * @export
@ -12478,6 +12703,17 @@ export class PartnerApi extends BaseAPI {
public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) { public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) {
return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); 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) { if (userResponseDto != null) {
Store.put(StoreKey.deviceId, deviceId); Store.put(StoreKey.deviceId, deviceId);
Store.put(StoreKey.deviceIdHash, fastHash(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.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken); Store.put(StoreKey.accessToken, accessToken);
shouldChangePassword = userResponseDto.shouldChangePassword; shouldChangePassword = userResponseDto.shouldChangePassword;
user = User.fromDto(userResponseDto); user = User.fromUserDto(userResponseDto);
retResult = true; retResult = true;
} else { } else {

View File

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

View File

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

View File

@ -18,11 +18,12 @@ class User {
this.isPartnerSharedWith = false, this.isPartnerSharedWith = false,
this.profileImagePath = '', this.profileImagePath = '',
this.memoryEnabled = true, this.memoryEnabled = true,
this.inTimeline = false,
}); });
Id get isarId => fastHash(id); Id get isarId => fastHash(id);
User.fromDto(UserResponseDto dto) User.fromUserDto(UserResponseDto dto)
: id = dto.id, : id = dto.id,
updatedAt = dto.updatedAt, updatedAt = dto.updatedAt,
email = dto.email, email = dto.email,
@ -34,6 +35,19 @@ class User {
isAdmin = dto.isAdmin, isAdmin = dto.isAdmin,
memoryEnabled = dto.memoriesEnabled; 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) @Index(unique: true, replace: false, type: IndexType.hash)
String id; String id;
DateTime updatedAt; DateTime updatedAt;
@ -45,6 +59,8 @@ class User {
bool isAdmin; bool isAdmin;
String profileImagePath; String profileImagePath;
bool? memoryEnabled; bool? memoryEnabled;
bool? inTimeline;
@Backlink(to: 'owner') @Backlink(to: 'owner')
final IsarLinks<Album> albums = IsarLinks<Album>(); final IsarLinks<Album> albums = IsarLinks<Album>();
@Backlink(to: 'sharedUsers') @Backlink(to: 'sharedUsers')
@ -62,7 +78,8 @@ class User {
isPartnerSharedWith == other.isPartnerSharedWith && isPartnerSharedWith == other.isPartnerSharedWith &&
profileImagePath == other.profileImagePath && profileImagePath == other.profileImagePath &&
isAdmin == other.isAdmin && isAdmin == other.isAdmin &&
memoryEnabled == other.memoryEnabled; memoryEnabled == other.memoryEnabled &&
inTimeline == other.inTimeline;
} }
@override @override
@ -77,5 +94,6 @@ class User {
isPartnerSharedWith.hashCode ^ isPartnerSharedWith.hashCode ^
profileImagePath.hashCode ^ profileImagePath.hashCode ^
isAdmin.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 { Future<List<User>?> _getAllUsers({required bool isAll}) async {
try { try {
final dto = await _apiService.userApi.getAllUsers(isAll); final dto = await _apiService.userApi.getAllUsers(isAll);
return dto?.map(User.fromDto).toList(); return dto?.map(User.fromUserDto).toList();
} catch (e) { } catch (e) {
_log.warning("Failed get all users:\n$e"); _log.warning("Failed get all users:\n$e");
return null; return null;

View File

@ -91,6 +91,7 @@ doc/OAuthCallbackDto.md
doc/OAuthConfigDto.md doc/OAuthConfigDto.md
doc/OAuthConfigResponseDto.md doc/OAuthConfigResponseDto.md
doc/PartnerApi.md doc/PartnerApi.md
doc/PartnerResponseDto.md
doc/PathEntityType.md doc/PathEntityType.md
doc/PathType.md doc/PathType.md
doc/PeopleResponseDto.md doc/PeopleResponseDto.md
@ -159,6 +160,7 @@ doc/TranscodePolicy.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md doc/UpdateAssetDto.md
doc/UpdateLibraryDto.md doc/UpdateLibraryDto.md
doc/UpdatePartnerDto.md
doc/UpdateStackParentDto.md doc/UpdateStackParentDto.md
doc/UpdateTagDto.md doc/UpdateTagDto.md
doc/UpdateUserDto.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_callback_dto.dart
lib/model/o_auth_config_dto.dart lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_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_entity_type.dart
lib/model/path_type.dart lib/model/path_type.dart
lib/model/people_response_dto.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_album_dto.dart
lib/model/update_asset_dto.dart lib/model/update_asset_dto.dart
lib/model/update_library_dto.dart lib/model/update_library_dto.dart
lib/model/update_partner_dto.dart
lib/model/update_stack_parent_dto.dart lib/model/update_stack_parent_dto.dart
lib/model/update_tag_dto.dart lib/model/update_tag_dto.dart
lib/model/update_user_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_dto_test.dart
test/o_auth_config_response_dto_test.dart test/o_auth_config_response_dto_test.dart
test/partner_api_test.dart test/partner_api_test.dart
test/partner_response_dto_test.dart
test/path_entity_type_test.dart test/path_entity_type_test.dart
test/path_type_test.dart test/path_type_test.dart
test/people_response_dto_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_album_dto_test.dart
test/update_asset_dto_test.dart test/update_asset_dto_test.dart
test/update_library_dto_test.dart test/update_library_dto_test.dart
test/update_partner_dto_test.dart
test/update_stack_parent_dto_test.dart test/update_stack_parent_dto_test.dart
test/update_tag_dto_test.dart test/update_tag_dto_test.dart
test/update_user_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" "type": "boolean"
} }
}, },
{
"name": "withPartners",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "timeBucket", "name": "timeBucket",
"required": true, "required": true,
@ -2199,6 +2207,14 @@
"type": "boolean" "type": "boolean"
} }
}, },
{
"name": "withPartners",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "key", "name": "key",
"required": false, "required": false,
@ -3491,7 +3507,7 @@
"application/json": { "application/json": {
"schema": { "schema": {
"items": { "items": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/PartnerResponseDto"
}, },
"type": "array" "type": "array"
} }
@ -3568,7 +3584,57 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "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" "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": { "PathEntityType": {
"enum": [ "enum": [
"asset", "asset",
@ -8982,6 +9119,17 @@
}, },
"type": "object" "type": "object"
}, },
"UpdatePartnerDto": {
"properties": {
"inTimeline": {
"type": "boolean"
}
},
"required": [
"inTimeline"
],
"type": "object"
},
"UpdateStackParentDto": { "UpdateStackParentDto": {
"properties": { "properties": {
"newParentId": { "newParentId": {

View File

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

View File

@ -10,6 +10,7 @@ import {
newCommunicationRepositoryMock, newCommunicationRepositoryMock,
newCryptoRepositoryMock, newCryptoRepositoryMock,
newJobRepositoryMock, newJobRepositoryMock,
newPartnerRepositoryMock,
newStorageRepositoryMock, newStorageRepositoryMock,
newSystemConfigRepositoryMock, newSystemConfigRepositoryMock,
} from '@test'; } from '@test';
@ -23,6 +24,7 @@ import {
ICommunicationRepository, ICommunicationRepository,
ICryptoRepository, ICryptoRepository,
IJobRepository, IJobRepository,
IPartnerRepository,
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
JobItem, JobItem,
@ -164,6 +166,7 @@ describe(AssetService.name, () => {
let storageMock: jest.Mocked<IStorageRepository>; let storageMock: jest.Mocked<IStorageRepository>;
let communicationMock: jest.Mocked<ICommunicationRepository>; let communicationMock: jest.Mocked<ICommunicationRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>; let configMock: jest.Mocked<ISystemConfigRepository>;
let partnerMock: jest.Mocked<IPartnerRepository>;
it('should work', () => { it('should work', () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
@ -177,7 +180,18 @@ describe(AssetService.name, () => {
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
storageMock = newStorageRepositoryMock(); storageMock = newStorageRepositoryMock();
configMock = newSystemConfigRepositoryMock(); 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) when(assetMock.getById)
.calledWith(assetStub.livePhotoStillAsset.id) .calledWith(assetStub.livePhotoStillAsset.id)
@ -327,7 +341,7 @@ describe(AssetService.name, () => {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
}), }),
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); ).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, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, isArchived: true,
userId: authStub.admin.id, userIds: [authStub.admin.id],
}); });
}); });
@ -380,9 +394,65 @@ describe(AssetService.name, () => {
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', 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', () => { describe('downloadFile', () => {

View File

@ -16,9 +16,11 @@ import {
ICommunicationRepository, ICommunicationRepository,
ICryptoRepository, ICryptoRepository,
IJobRepository, IJobRepository,
IPartnerRepository,
IStorageRepository, IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
ImmichReadStream, ImmichReadStream,
TimeBucketOptions,
} from '../repositories'; } from '../repositories';
import { StorageCore, StorageFolder } from '../storage'; import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config'; import { SystemConfigCore } from '../system-config';
@ -83,6 +85,7 @@ export class AssetService {
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) { ) {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
this.configCore = SystemConfigCore.create(configRepository); this.configCore = SystemConfigCore.create(configRepository);
@ -187,11 +190,25 @@ export class AssetService {
await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]); 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[]> { async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(authUser, dto); await this.timeBucketChecks(authUser, dto);
return this.assetRepository.getTimeBuckets(dto); const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto);
return this.assetRepository.getTimeBuckets(timeBucketOptions);
} }
async getTimeBucket( async getTimeBucket(
@ -199,7 +216,8 @@ export class AssetService {
dto: TimeBucketAssetDto, dto: TimeBucketAssetDto,
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> { ): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
await this.timeBucketChecks(authUser, dto); 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) { if (authUser.isShowMetadata) {
return assets.map((asset) => mapAsset(asset, { withStack: true })); return assets.map((asset) => mapAsset(asset, { withStack: true }));
} else { } 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> { async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id); await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);

View File

@ -38,6 +38,11 @@ export class TimeBucketDto {
@IsBoolean() @IsBoolean()
@Transform(toBoolean) @Transform(toBoolean)
withStacked?: boolean; withStacked?: boolean;
@Optional()
@IsBoolean()
@Transform(toBoolean)
withPartners?: boolean;
} }
export class TimeBucketAssetDto extends TimeBucketDto { 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 { BadRequestException } from '@nestjs/common';
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
import { UserResponseDto } from '../index'; import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories';
import { IPartnerRepository, PartnerDirection } from '../repositories'; import { PartnerResponseDto } from './partner.dto';
import { PartnerService } from './partner.service'; import { PartnerService } from './partner.service';
const responseDto = { const responseDto = {
admin: <UserResponseDto>{ admin: <PartnerResponseDto>{
email: 'admin@test.com', email: 'admin@test.com',
firstName: 'admin_first_name', firstName: 'admin_first_name',
id: 'admin_id', id: 'admin_id',
@ -20,8 +20,9 @@ const responseDto = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
externalPath: null, externalPath: null,
memoriesEnabled: true, memoriesEnabled: true,
inTimeline: true,
}, },
user1: <UserResponseDto>{ user1: <PartnerResponseDto>{
email: 'immich@test.com', email: 'immich@test.com',
firstName: 'immich_first_name', firstName: 'immich_first_name',
id: 'user-id', id: 'user-id',
@ -36,16 +37,18 @@ const responseDto = {
updatedAt: new Date('2021-01-01'), updatedAt: new Date('2021-01-01'),
externalPath: null, externalPath: null,
memoriesEnabled: true, memoriesEnabled: true,
inTimeline: true,
}, },
}; };
describe(PartnerService.name, () => { describe(PartnerService.name, () => {
let sut: PartnerService; let sut: PartnerService;
let partnerMock: jest.Mocked<IPartnerRepository>; let partnerMock: jest.Mocked<IPartnerRepository>;
let accessMock: jest.Mocked<IAccessRepository>;
beforeEach(async () => { beforeEach(async () => {
partnerMock = newPartnerRepositoryMock(); partnerMock = newPartnerRepositoryMock();
sut = new PartnerService(partnerMock); sut = new PartnerService(partnerMock, accessMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,14 +1,22 @@
import { PartnerEntity } from '@app/infra/entities'; import { PartnerEntity } from '@app/infra/entities';
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from '../access';
import { AuthUserDto } from '../auth'; import { AuthUserDto } from '../auth';
import { IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories';
import { UserResponseDto, mapUser } from '../user'; import { mapUser } from '../user';
import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto';
@Injectable() @Injectable()
export class PartnerService { 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 partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId };
const exists = await this.repository.get(partnerId); const exists = await this.repository.get(partnerId);
if (exists) { if (exists) {
@ -29,7 +37,7 @@ export class PartnerService {
await this.repository.remove(partner); 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 partners = await this.repository.getAll(authUser.id);
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners return partners
@ -38,8 +46,22 @@ export class PartnerService {
.map((partner) => this.map(partner, direction)); .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" // 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: { person: {
hasOwnerAccess(userId: string, personId: string): Promise<boolean>; 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; isTrashed?: boolean;
albumId?: string; albumId?: string;
personId?: string; personId?: string;
userId?: string; userIds?: string[];
withStacked?: boolean; withStacked?: boolean;
} }

View File

@ -17,4 +17,5 @@ export interface IPartnerRepository {
get(partner: PartnerIds): Promise<PartnerEntity | null>; get(partner: PartnerIds): Promise<PartnerEntity | null>;
create(partner: PartnerIds): Promise<PartnerEntity>; create(partner: PartnerIds): Promise<PartnerEntity>;
remove(entity: PartnerEntity): Promise<void>; 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 { AuthUserDto, PartnerDirection, PartnerService } from '@app/domain';
import { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common'; 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 { ApiQuery, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated } from '../app.guard'; import { AuthUser, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils'; import { UseValidation } from '../app.utils';
@ -17,15 +18,24 @@ export class PartnerController {
getPartners( getPartners(
@AuthUser() authUser: AuthUserDto, @AuthUser() authUser: AuthUserDto,
@Query('direction') direction: PartnerDirection, @Query('direction') direction: PartnerDirection,
): Promise<UserResponseDto[]> { ): Promise<PartnerResponseDto[]> {
return this.service.getAll(authUser, direction); return this.service.getAll(authUser, direction);
} }
@Post(':id') @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); 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') @Delete(':id')
removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> { removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(authUser, id); 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'; import { UserEntity } from './user.entity';
@ -23,4 +23,7 @@ export class PartnerEntity {
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; 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) { 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 let builder = this.repository
.createQueryBuilder('asset') .createQueryBuilder('asset')
@ -532,11 +532,11 @@ export class AssetRepository implements IAssetRepository {
builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId }); builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId });
} }
if (userId) { if (userIds) {
builder = builder.andWhere('asset.ownerId = :userId', { userId }); builder = builder.andWhere('asset.ownerId IN (:...userIds )', { userIds });
} }
if (isArchived != undefined) { if (isArchived !== undefined) {
builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived }); builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived });
} }

View File

@ -1,7 +1,7 @@
import { IPartnerRepository, PartnerIds } from '@app/domain'; import { IPartnerRepository, PartnerIds } from '@app/domain';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { DeepPartial, Repository } from 'typeorm';
import { PartnerEntity } from '../entities'; import { PartnerEntity } from '../entities';
@Injectable() @Injectable()
@ -16,12 +16,22 @@ export class PartnerRepository implements IPartnerRepository {
return this.repository.findOne({ where: { sharedById, sharedWithId } }); return this.repository.findOne({ where: { sharedById, sharedWithId } });
} }
async create({ sharedById, sharedWithId }: PartnerIds): Promise<PartnerEntity> { create({ sharedById, sharedWithId }: PartnerIds): Promise<PartnerEntity> {
await this.repository.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); return this.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } });
return this.repository.findOneOrFail({ where: { sharedById, sharedWithId } });
} }
async remove(entity: PartnerEntity): Promise<void> { async remove(entity: PartnerEntity): Promise<void> {
await this.repository.remove(entity); 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', () => { 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', () => { describe('DELETE /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).delete(`/partner/${user2.userId}`); const { status, body } = await request(server).delete(`/partner/${user2.userId}`);

View File

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

View File

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

View File

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

View File

@ -2361,6 +2361,103 @@ export interface OAuthConfigResponseDto {
*/ */
'url'?: string; '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 * @export
@ -4220,6 +4317,19 @@ export interface UpdateLibraryDto {
*/ */
'name'?: string; 'name'?: string;
} }
/**
*
* @export
* @interface UpdatePartnerDto
*/
export interface UpdatePartnerDto {
/**
*
* @type {boolean}
* @memberof UpdatePartnerDto
*/
'inTimeline': boolean;
}
/** /**
* *
* @export * @export
@ -7274,11 +7384,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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 // verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBucket', 'size', size) assertParamExists('getTimeBucket', 'size', size)
// verify required parameter 'timeBucket' is not null or undefined // verify required parameter 'timeBucket' is not null or undefined
@ -7336,6 +7447,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked; localVarQueryParameter['withStacked'] = withStacked;
} }
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (timeBucket !== undefined) { if (timeBucket !== undefined) {
localVarQueryParameter['timeBucket'] = timeBucket; localVarQueryParameter['timeBucket'] = timeBucket;
} }
@ -7365,11 +7480,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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 // verify required parameter 'size' is not null or undefined
assertParamExists('getTimeBuckets', 'size', size) assertParamExists('getTimeBuckets', 'size', size)
const localVarPath = `/asset/time-buckets`; const localVarPath = `/asset/time-buckets`;
@ -7425,6 +7541,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarQueryParameter['withStacked'] = withStacked; localVarQueryParameter['withStacked'] = withStacked;
} }
if (withPartners !== undefined) {
localVarQueryParameter['withPartners'] = withPartners;
}
if (key !== undefined) { if (key !== undefined) {
localVarQueryParameter['key'] = key; localVarQueryParameter['key'] = key;
} }
@ -8227,12 +8347,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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>>> { 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, key, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -8245,12 +8366,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {boolean} [isFavorite] * @param {boolean} [isFavorite]
* @param {boolean} [isTrashed] * @param {boolean} [isTrashed]
* @param {boolean} [withStacked] * @param {boolean} [withStacked]
* @param {boolean} [withPartners]
* @param {string} [key] * @param {string} [key]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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>>> { 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, key, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -8547,7 +8669,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @throws {RequiredError} * @throws {RequiredError}
*/ */
getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { 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} * @throws {RequiredError}
*/ */
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { 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. * Get all asset of a device that are in the database, ID only.
@ -9043,6 +9165,13 @@ export interface AssetApiGetTimeBucketRequest {
*/ */
readonly withStacked?: boolean readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBucket
*/
readonly withPartners?: boolean
/** /**
* *
* @type {string} * @type {string}
@ -9113,6 +9242,13 @@ export interface AssetApiGetTimeBucketsRequest {
*/ */
readonly withStacked?: boolean readonly withStacked?: boolean
/**
*
* @type {boolean}
* @memberof AssetApiGetTimeBuckets
*/
readonly withPartners?: boolean
/** /**
* *
* @type {string} * @type {string}
@ -9592,7 +9728,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi * @memberof AssetApi
*/ */
public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) { 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 * @memberof AssetApi
*/ */
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { 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 : {}; let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.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 { return {
url: toPathString(localVarUrlObj), url: toPathString(localVarUrlObj),
options: localVarRequestOptions, options: localVarRequestOptions,
@ -12333,7 +12517,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@ -12343,7 +12527,7 @@ export const PartnerApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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); const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
@ -12357,6 +12541,17 @@ export const PartnerApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options); const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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)); 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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @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)); 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> { removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise<void> {
return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath)); 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 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 * PartnerApi - object-oriented interface
* @export * @export
@ -12478,6 +12703,17 @@ export class PartnerApi extends BaseAPI {
public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) { public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) {
return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); 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> </script>
<header> <header>
{#if $isMultiSelectState} {#if $isMultiSelectState && user}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar
ownerId={user.id}
assets={$selectedAssets}
clearSelect={() => assetInteractionStore.clearMultiselect()}
>
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
{#if sharedLink.allowDownload} {#if sharedLink.allowDownload}
<DownloadAction filename="{album.albumName}.zip" /> <DownloadAction filename="{album.albumName}.zip" />

View File

@ -20,14 +20,14 @@
let loading = false; let loading = false;
const { getAssets, clearSelect } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleArchive = async () => { const handleArchive = async () => {
const isArchived = !unarchive; const isArchived = !unarchive;
loading = true; loading = true;
try { 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); const ids = assets.map(({ id }) => id);
if (ids.length > 0) { if (ids.length > 0) {

View File

@ -14,13 +14,13 @@
AssetJobName.TranscodeVideo, 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) => { const handleRunJob = async (name: AssetJobName) => {
try { 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 } }); await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info }); notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info });
clearSelect(); clearSelect();

View File

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

View File

@ -20,14 +20,15 @@
let loading = false; let loading = false;
const { getAssets, clearSelect } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleFavorite = async () => { const handleFavorite = async () => {
const isFavorite = !removeFavorite; const isFavorite = !removeFavorite;
loading = true; loading = true;
try { 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); const ids = assets.map(({ id }) => id);
if (ids.length > 0) { if (ids.length > 0) {

View File

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

View File

@ -9,7 +9,8 @@
export interface AssetControlContext { export interface AssetControlContext {
// Wrap assets in a function, because context isn't reactive. // 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; clearSelect: () => void;
} }
@ -25,8 +26,14 @@
export let assets: Set<AssetResponseDto>; export let assets: Set<AssetResponseDto>;
export let clearSelect: () => void; 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> </script>
<ControlAppBar on:close-button-click={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> <ControlAppBar on:close-button-click={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">

View File

@ -1,22 +1,71 @@
<script lang="ts"> <script lang="ts">
import { UserResponseDto, api } from '@api'; import { PartnerResponseDto, UserResponseDto, api } from '@api';
import UserAvatar from '../shared-components/user-avatar.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import PartnerSelectionModal from './partner-selection-modal.svelte'; import PartnerSelectionModal from './partner-selection-modal.svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.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 user: UserResponseDto;
export let partners: UserResponseDto[];
let createPartner = false; let createPartner = false;
let removePartner: UserResponseDto | null = null; let removePartner: PartnerResponseDto | null = null;
let partners: Array<PartnerSharing> = [];
onMount(() => {
refreshPartners();
});
const refreshPartners = async () => { const refreshPartners = async () => {
const { data } = await api.partnerApi.getPartners({ direction: 'shared-by' }); partners = [];
partners = data;
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 () => { const handleRemovePartner = async () => {
@ -45,34 +94,80 @@
handleError(error, 'Unable to add partners'); 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> </script>
<section class="my-4"> <section class="my-4">
{#if partners.length > 0} {#if partners.length > 0}
<div class="flex flex-row gap-4"> {#each partners as partner (partner.user.id)}
{#each partners as partner (partner.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 px-5 py-4 transition-all"> <div class="flex gap-4 rounded-lg pb-4 transition-all justify-between">
<UserAvatar user={partner} size="md" autoColor /> <div class="flex gap-4">
<div class="text-left"> <UserAvatar user={partner.user} size="md" autoColor />
<p class="text-immich-fg dark:text-immich-dark-fg"> <div class="text-left">
{partner.firstName} <p class="text-immich-fg dark:text-immich-dark-fg">
{partner.lastName} {partner.user.firstName}
</p> {partner.user.lastName}
<p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75"> </p>
{partner.email} <p class="text-xs text-immich-fg/75 dark:text-immich-dark-fg/75">
</p> {partner.user.email}
</p>
</div>
</div> </div>
<CircleIconButton
on:click={() => (removePartner = partner)} {#if partner.sharedByMe}
icon={mdiClose} <CircleIconButton
size={'16'} on:click={() => (removePartner = partner.user)}
title="Remove partner" icon={mdiClose}
/> size={'16'}
title="Stop sharing your photos with this user"
/>
{/if}
</div> </div>
{/each}
</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}
{/if} {/if}
<div class="flex justify-end">
<div class="flex justify-end mt-5">
<Button size="sm" on:click={() => (createPartner = true)}>Add partner</Button> <Button size="sm" on:click={() => (createPartner = true)}>Add partner</Button>
</div> </div>
</section> </section>

View File

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

View File

@ -16,7 +16,7 @@
export let data: PageData; 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 assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;

View File

@ -26,7 +26,7 @@
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
let handleEscapeKey = false; 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 assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@ -48,7 +48,11 @@
</script> </script>
{#if $isMultiSelectState} {#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar
ownerId={data.user.id}
assets={$selectedAssets}
clearSelect={() => assetInteractionStore.clearMultiselect()}
>
<CreateSharedLink on:escape={() => (handleEscapeKey = true)} /> <CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={mdiPlus} title="Add"> <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: keys } = await locals.api.keyApi.getApiKeys();
const { data: devices } = await locals.api.authenticationApi.getAuthDevices(); const { data: devices } = await locals.api.authenticationApi.getAuthDevices();
const { data: partners } = await locals.api.partnerApi.getPartners({ direction: 'shared-by' });
return { return {
user, user,
keys, keys,
devices, devices,
partners,
meta: { meta: {
title: 'Settings', title: 'Settings',
}, },

View File

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